1package zip
2
3import (
4	"crypto/sha256"
5	"encoding/hex"
6	"fmt"
7	"io"
8	"net/http"
9	"net/http/httptest"
10	"os"
11	"testing"
12	"time"
13
14	"github.com/stretchr/testify/require"
15
16	"gitlab.com/gitlab-org/gitlab-pages/internal/config"
17	"gitlab.com/gitlab-org/gitlab-pages/internal/serving"
18	"gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers"
19)
20
21func TestZip_ServeFileHTTP(t *testing.T) {
22	testServerURL, cleanup := newZipFileServerURL(t, "group/zip.gitlab.io/public-without-dirs.zip")
23	defer cleanup()
24
25	wd, err := os.Getwd()
26	require.NoError(t, err)
27
28	httpURL := testServerURL + "/public.zip"
29	fileURL := "file://" + wd + "/group/zip.gitlab.io/public-without-dirs.zip"
30
31	tests := map[string]struct {
32		vfsPath        string
33		path           string
34		expectedStatus int
35		expectedBody   string
36		extraHeaders   http.Header
37	}{
38		"accessing /index.html": {
39			vfsPath:        httpURL,
40			path:           "/index.html",
41			expectedStatus: http.StatusOK,
42			expectedBody:   "zip.gitlab.io/project/index.html\n",
43		},
44		"accessing /index.html from disk": {
45			vfsPath:        fileURL,
46			path:           "/index.html",
47			expectedStatus: http.StatusOK,
48			expectedBody:   "zip.gitlab.io/project/index.html\n",
49		},
50		"accessing /": {
51			vfsPath:        httpURL,
52			path:           "/",
53			expectedStatus: http.StatusOK,
54			expectedBody:   "zip.gitlab.io/project/index.html\n",
55		},
56		"accessing / If-Modified-Since": {
57			vfsPath:        httpURL,
58			path:           "/",
59			expectedStatus: http.StatusNotModified,
60			extraHeaders: http.Header{
61				"If-Modified-Since": {time.Now().Format(http.TimeFormat)},
62			},
63		},
64		"accessing / If-Modified-Since fails": {
65			vfsPath:        httpURL,
66			path:           "/",
67			expectedStatus: http.StatusOK,
68			expectedBody:   "zip.gitlab.io/project/index.html\n",
69			extraHeaders: http.Header{
70				"If-Modified-Since": {time.Now().AddDate(-10, 0, 0).Format(http.TimeFormat)},
71			},
72		},
73		"accessing / If-Unmodified-Since": {
74			vfsPath:        httpURL,
75			path:           "/",
76			expectedStatus: http.StatusPreconditionFailed,
77			extraHeaders: http.Header{
78				"If-Unmodified-Since": {time.Now().AddDate(-10, 0, 0).Format(http.TimeFormat)},
79			},
80		},
81		"accessing / If-Unmodified-Since fails": {
82			vfsPath:        httpURL,
83			path:           "/",
84			expectedStatus: http.StatusOK,
85			expectedBody:   "zip.gitlab.io/project/index.html\n",
86			extraHeaders: http.Header{
87				"If-Unmodified-Since": {time.Now().Format(http.TimeFormat)},
88			},
89		},
90		"accessing / from disk": {
91			vfsPath:        fileURL,
92			path:           "/",
93			expectedStatus: http.StatusOK,
94			expectedBody:   "zip.gitlab.io/project/index.html\n",
95		},
96		"accessing without /": {
97			vfsPath:        httpURL,
98			path:           "",
99			expectedStatus: http.StatusFound,
100			expectedBody:   "<a href=\"//zip.gitlab.io/zip/\">Found</a>.\n\n",
101		},
102		"accessing without / from disk": {
103			vfsPath:        fileURL,
104			path:           "",
105			expectedStatus: http.StatusFound,
106			expectedBody:   "<a href=\"//zip.gitlab.io/zip/\">Found</a>.\n\n",
107		},
108		"accessing archive that is 404": {
109			vfsPath: testServerURL + "/invalid.zip",
110			path:    "/index.html",
111			// we expect the status to not be set
112			expectedStatus: 0,
113		},
114		"accessing archive that is 500": {
115			vfsPath:        testServerURL + "/500",
116			path:           "/index.html",
117			expectedStatus: http.StatusInternalServerError,
118		},
119		"accessing file:// outside of allowedPaths": {
120			vfsPath:        "file:///some/file/outside/path",
121			path:           "/index.html",
122			expectedStatus: http.StatusInternalServerError,
123		},
124		"accessing / If-None-Match": {
125			vfsPath:        httpURL,
126			path:           "/",
127			expectedStatus: http.StatusNotModified,
128			extraHeaders: http.Header{
129				"If-None-Match": {fmt.Sprintf("%q", sha(httpURL))},
130			},
131		},
132		"accessing / If-None-Match fails": {
133			vfsPath:        httpURL,
134			path:           "/",
135			expectedStatus: http.StatusOK,
136			expectedBody:   "zip.gitlab.io/project/index.html\n",
137			extraHeaders: http.Header{
138				"If-None-Match": {fmt.Sprintf("%q", "badetag")},
139			},
140		},
141		"accessing / If-Match": {
142			vfsPath:        httpURL,
143			path:           "/",
144			expectedStatus: http.StatusOK,
145			expectedBody:   "zip.gitlab.io/project/index.html\n",
146			extraHeaders: http.Header{
147				"If-Match": {fmt.Sprintf("%q", sha(httpURL))},
148			},
149		},
150		"accessing / If-Match fails": {
151			vfsPath:        httpURL,
152			path:           "/",
153			expectedStatus: http.StatusPreconditionFailed,
154			extraHeaders: http.Header{
155				"If-Match": {fmt.Sprintf("%q", "wrongetag")},
156			},
157		},
158		"accessing / If-Match fails2": {
159			vfsPath:        httpURL,
160			path:           "/",
161			expectedStatus: http.StatusPreconditionFailed,
162			extraHeaders: http.Header{
163				"If-Match": {","},
164			},
165		},
166	}
167
168	cfg := &config.Config{
169		Zip: config.ZipServing{
170			ExpirationInterval: 10 * time.Second,
171			CleanupInterval:    5 * time.Second,
172			RefreshInterval:    5 * time.Second,
173			OpenTimeout:        5 * time.Second,
174			AllowedPaths:       []string{wd},
175		},
176	}
177
178	s := Instance()
179	err = s.Reconfigure(cfg)
180	require.NoError(t, err)
181
182	for name, test := range tests {
183		t.Run(name, func(t *testing.T) {
184			w := httptest.NewRecorder()
185			w.Code = 0 // ensure that code is not set, and it is being set by handler
186			r := httptest.NewRequest(http.MethodGet, "http://zip.gitlab.io/zip"+test.path, nil)
187
188			if test.extraHeaders != nil {
189				r.Header = test.extraHeaders
190			}
191
192			handler := serving.Handler{
193				Writer:  w,
194				Request: r,
195				LookupPath: &serving.LookupPath{
196					Prefix: "/zip/",
197					Path:   test.vfsPath,
198					SHA256: sha(test.vfsPath),
199				},
200				SubPath: test.path,
201			}
202
203			if test.expectedStatus == 0 {
204				require.False(t, s.ServeFileHTTP(handler))
205				require.Zero(t, w.Code, "we expect status to not be set")
206				return
207			}
208
209			require.True(t, s.ServeFileHTTP(handler))
210
211			resp := w.Result()
212			defer resp.Body.Close()
213
214			require.Equal(t, test.expectedStatus, resp.StatusCode)
215			body, err := io.ReadAll(resp.Body)
216			require.NoError(t, err)
217
218			if test.expectedStatus == http.StatusOK {
219				require.NotEmpty(t, resp.Header.Get("Last-Modified"))
220				require.NotEmpty(t, resp.Header.Get("ETag"))
221			}
222
223			if test.expectedStatus != http.StatusInternalServerError {
224				require.Equal(t, test.expectedBody, string(body))
225			}
226		})
227	}
228}
229
230func sha(path string) string {
231	sha := sha256.Sum256([]byte(path))
232	s := hex.EncodeToString(sha[:])
233	return s
234}
235
236var chdirSet = false
237
238func newZipFileServerURL(t *testing.T, zipFilePath string) (string, func()) {
239	t.Helper()
240
241	chdir := testhelpers.ChdirInPath(t, "../../../../shared/pages", &chdirSet)
242
243	m := http.NewServeMux()
244	m.HandleFunc("/public.zip", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
245		http.ServeFile(w, r, zipFilePath)
246	}))
247	m.HandleFunc("/500", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
248		w.WriteHeader(http.StatusInternalServerError)
249	}))
250
251	testServer := httptest.NewServer(m)
252
253	return testServer.URL, func() {
254		chdir()
255		testServer.Close()
256	}
257}
258