diff options
Diffstat (limited to 'internal/source/disk/domain_test.go')
-rw-r--r-- | internal/source/disk/domain_test.go | 435 |
1 files changed, 435 insertions, 0 deletions
diff --git a/internal/source/disk/domain_test.go b/internal/source/disk/domain_test.go new file mode 100644 index 00000000..80772d37 --- /dev/null +++ b/internal/source/disk/domain_test.go @@ -0,0 +1,435 @@ +package disk + +import ( + "compress/gzip" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/domain" + "gitlab.com/gitlab-org/gitlab-pages/internal/fixture" + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" +) + +func serveFileOrNotFound(domain *domain.Domain) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !domain.ServeFileHTTP(w, r) { + domain.ServeNotFoundHTTP(w, r) + } + } +} + +func testGroupServeHTTPHost(t *testing.T, host string) { + testGroup := &domain.Domain{ + Location: "group", + Resolver: &Group{ + name: "group", + projects: map[string]*projectConfig{ + "group.test.io": &projectConfig{}, + "group.gitlab-example.com": &projectConfig{}, + "project": &projectConfig{}, + "project2": &projectConfig{}, + }, + }, + } + + makeURL := func(path string) string { + return "http://" + host + path + } + + serve := serveFileOrNotFound(testGroup) + + require.HTTPBodyContains(t, serve, "GET", makeURL("/"), nil, "main-dir") + require.HTTPBodyContains(t, serve, "GET", makeURL("/index"), nil, "main-dir") + require.HTTPBodyContains(t, serve, "GET", makeURL("/index.html"), nil, "main-dir") + testhelpers.AssertRedirectTo(t, serve, "GET", makeURL("/project"), nil, "//"+host+"/project/") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project/"), nil, "project-subdir") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project/index"), nil, "project-subdir") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project/index/"), nil, "project-subdir") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project/index.html"), nil, "project-subdir") + testhelpers.AssertRedirectTo(t, serve, "GET", makeURL("/project/subdir"), nil, "//"+host+"/project/subdir/") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project/subdir/"), nil, "project-subsubdir") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project2/"), nil, "project2-main") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project2/index"), nil, "project2-main") + require.HTTPBodyContains(t, serve, "GET", makeURL("/project2/index.html"), nil, "project2-main") + require.HTTPError(t, serve, "GET", makeURL("/private.project/"), nil) + require.HTTPError(t, serve, "GET", makeURL("//about.gitlab.com/%2e%2e"), nil) + require.HTTPError(t, serve, "GET", makeURL("/symlink"), nil) + require.HTTPError(t, serve, "GET", makeURL("/symlink/index.html"), nil) + require.HTTPError(t, serve, "GET", makeURL("/symlink/subdir/"), nil) + require.HTTPError(t, serve, "GET", makeURL("/project/fifo"), nil) + require.HTTPError(t, serve, "GET", makeURL("/not-existing-file"), nil) + require.HTTPRedirect(t, serve, "GET", makeURL("/project//about.gitlab.com/%2e%2e"), nil) +} + +func TestGroupServeHTTP(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + t.Run("group.test.io", func(t *testing.T) { testGroupServeHTTPHost(t, "group.test.io") }) + t.Run("group.test.io:8080", func(t *testing.T) { testGroupServeHTTPHost(t, "group.test.io:8080") }) +} + +func TestDomainServeHTTP(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + testDomain := &domain.Domain{ + Name: "test.domain.com", + Location: "group/project2", + Resolver: &customProjectResolver{ + config: &domainConfig{}, + }, + } + + require.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/", nil, "project2-main") + require.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/index.html", nil, "project2-main") + require.HTTPRedirect(t, serveFileOrNotFound(testDomain), "GET", "/subdir", nil) + require.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir", nil, + `<a href="/subdir/">Found</a>`) + require.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir/", nil, "project2-subdir") + require.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir/index.html", nil, "project2-subdir") + require.HTTPError(t, serveFileOrNotFound(testDomain), "GET", "//about.gitlab.com/%2e%2e", nil) + require.HTTPError(t, serveFileOrNotFound(testDomain), "GET", "/not-existing-file", nil) +} + +func TestIsHTTPSOnly(t *testing.T) { + tests := []struct { + name string + domain *domain.Domain + url string + expected bool + }{ + { + name: "Default group domain with HTTPS-only enabled", + domain: &domain.Domain{ + Location: "group/project", + Resolver: &Group{ + name: "group", + projects: projects{"test-domain": &projectConfig{HTTPSOnly: true}}, + }, + }, + url: "http://test-domain", + expected: true, + }, + { + name: "Default group domain with HTTPS-only disabled", + domain: &domain.Domain{ + Location: "group/project", + Resolver: &Group{ + name: "group", + projects: projects{"test-domain": &projectConfig{HTTPSOnly: false}}, + }, + }, + url: "http://test-domain", + expected: false, + }, + { + name: "Case-insensitive default group domain with HTTPS-only enabled", + domain: &domain.Domain{ + Location: "group/project", + Resolver: &Group{ + name: "group", + projects: projects{"test-domain": &projectConfig{HTTPSOnly: true}}, + }, + }, + url: "http://Test-domain", + expected: true, + }, + { + name: "Other group domain with HTTPS-only enabled", + domain: &domain.Domain{ + Location: "group/project", + Resolver: &Group{ + name: "group", + projects: projects{"project": &projectConfig{HTTPSOnly: true}}, + }, + }, + url: "http://test-domain/project", + expected: true, + }, + { + name: "Other group domain with HTTPS-only disabled", + domain: &domain.Domain{ + Location: "group/project", + Resolver: &Group{ + name: "group", + projects: projects{"project": &projectConfig{HTTPSOnly: false}}, + }, + }, + url: "http://test-domain/project", + expected: false, + }, + { + name: "Unknown project", + domain: &domain.Domain{ + Location: "group/project", + }, + url: "http://test-domain/project", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, test.url, nil) + require.Equal(t, test.expected, test.domain.IsHTTPSOnly(req)) + }) + } +} + +func testHTTPGzip(t *testing.T, handler http.HandlerFunc, mode, url string, values url.Values, acceptEncoding string, str interface{}, contentType string, ungzip bool) { + w := httptest.NewRecorder() + req, err := http.NewRequest(mode, url+"?"+values.Encode(), nil) + require.NoError(t, err) + if acceptEncoding != "" { + req.Header.Add("Accept-Encoding", acceptEncoding) + } + handler(w, req) + + if ungzip { + reader, err := gzip.NewReader(w.Body) + require.NoError(t, err) + defer reader.Close() + + contentEncoding := w.Header().Get("Content-Encoding") + require.Equal(t, "gzip", contentEncoding, "Content-Encoding") + + bytes, err := ioutil.ReadAll(reader) + require.NoError(t, err) + require.Contains(t, string(bytes), str) + } else { + require.Contains(t, w.Body.String(), str) + } + + require.Equal(t, contentType, w.Header().Get("Content-Type")) +} + +func TestGroupServeHTTPGzip(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + testGroup := &domain.Domain{ + Location: "group", + Resolver: &Group{ + name: "group", + projects: map[string]*projectConfig{ + "group.test.io": &projectConfig{}, + "group.gitlab-example.com": &projectConfig{}, + "project": &projectConfig{}, + "project2": &projectConfig{}, + }, + }, + } + + testSet := []struct { + mode string // HTTP mode + url string // Test URL + acceptEncoding string // Accept encoding header + body interface{} // Expected body at above URL + contentType string // Expected content-type + ungzip bool // Expect the response to be gzipped? + }{ + // No gzip encoding requested + {"GET", "/index.html", "", "main-dir", "text/html; charset=utf-8", false}, + {"GET", "/index.html", "identity", "main-dir", "text/html; charset=utf-8", false}, + {"GET", "/index.html", "gzip; q=0", "main-dir", "text/html; charset=utf-8", false}, + // gzip encoding requested, + {"GET", "/index.html", "*", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "identity, gzip", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "gzip", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "gzip; q=1", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "gzip; q=0.9", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "gzip, deflate", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "gzip; q=1, deflate", "main-dir", "text/html; charset=utf-8", true}, + {"GET", "/index.html", "gzip; q=0.9, deflate", "main-dir", "text/html; charset=utf-8", true}, + // gzip encoding requested, but url does not have compressed content on disk + {"GET", "/project2/index.html", "*", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "identity, gzip", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "gzip", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "gzip; q=1", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "gzip; q=0.9", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "gzip, deflate", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "gzip; q=1, deflate", "project2-main", "text/html; charset=utf-8", false}, + {"GET", "/project2/index.html", "gzip; q=0.9, deflate", "project2-main", "text/html; charset=utf-8", false}, + // malformed headers + {"GET", "/index.html", ";; gzip", "main-dir", "text/html; charset=utf-8", false}, + {"GET", "/index.html", "middle-out", "main-dir", "text/html; charset=utf-8", false}, + {"GET", "/index.html", "gzip; quality=1", "main-dir", "text/html; charset=utf-8", false}, + // Symlinked .gz files are not supported + {"GET", "/gz-symlink", "*", "data", "text/plain; charset=utf-8", false}, + // Unknown file-extension, with text content + {"GET", "/text.unknown", "*", "hello", "text/plain; charset=utf-8", true}, + {"GET", "/text-nogzip.unknown", "*", "hello", "text/plain; charset=utf-8", false}, + // Unknown file-extension, with PNG content + {"GET", "/image.unknown", "*", "GIF89a", "image/gif", true}, + {"GET", "/image-nogzip.unknown", "*", "GIF89a", "image/gif", false}, + } + + for _, tt := range testSet { + URL := "http://group.test.io" + tt.url + testHTTPGzip(t, serveFileOrNotFound(testGroup), tt.mode, URL, nil, tt.acceptEncoding, tt.body, tt.contentType, tt.ungzip) + } +} + +func TestGroup404ServeHTTP(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + testGroup := &domain.Domain{ + Location: "group.404", + Resolver: &Group{ + name: "group.404", + projects: map[string]*projectConfig{ + "domain.404": &projectConfig{}, + "group.404.test.io": &projectConfig{}, + "project.404": &projectConfig{}, + "project.404.symlink": &projectConfig{}, + "project.no.404": &projectConfig{}, + }, + }, + } + + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/not/existing-file", nil, "Custom 404 project page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/", nil, "Custom 404 project page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not/existing-file", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/", nil, "Custom 404 group page") + require.HTTPBodyNotContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404.symlink/not/existing-file", nil, "Custom 404 project page") + + // Ensure the namespace project's custom 404.html is not used by projects + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.no.404/not/existing-file", nil, "The page you're looking for could not be found.") +} + +func TestDomain404ServeHTTP(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + testDomain := &domain.Domain{ + Location: "group.404/domain.404", + Resolver: &customProjectResolver{ + config: &domainConfig{Domain: "domain.404.com"}, + }, + } + + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom 404 group page") +} + +func TestPredefined404ServeHTTP(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + testDomain := &domain.Domain{ + Location: "group", + } + + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found") +} + +func TestGroupCertificate(t *testing.T) { + testGroup := &domain.Domain{ + Location: "group", + } + + tls, err := testGroup.EnsureCertificate() + require.Nil(t, tls) + require.Error(t, err) +} + +func TestDomainNoCertificate(t *testing.T) { + testDomain := &domain.Domain{ + Location: "group/project2", + Resolver: &customProjectResolver{ + config: &domainConfig{Domain: "test.domain.com"}, + }, + } + + tls, err := testDomain.EnsureCertificate() + require.Nil(t, tls) + require.Error(t, err) + + _, err2 := testDomain.EnsureCertificate() + require.Error(t, err) + require.Equal(t, err, err2) +} + +func TestDomainCertificate(t *testing.T) { + testDomain := &domain.Domain{ + Customized: true, // TODO + Location: "group/project2", + Name: "test.domain.com", + CertificateCert: fixture.Certificate, + CertificateKey: fixture.Key, + Resolver: &customProjectResolver{}, + } + + tls, err := testDomain.EnsureCertificate() + require.NotNil(t, tls) + require.NoError(t, err) +} + +func TestCacheControlHeaders(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + testGroup := &domain.Domain{ + Location: "group", + Resolver: &Group{ + name: "group", + projects: map[string]*projectConfig{ + "group.test.io": &projectConfig{}, + }, + }, + } + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://group.test.io/", nil) + require.NoError(t, err) + + now := time.Now() + serveFileOrNotFound(testGroup)(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "max-age=600", w.Header().Get("Cache-Control")) + + expires := w.Header().Get("Expires") + require.NotEmpty(t, expires) + + expiresTime, err := time.Parse(time.RFC1123, expires) + require.NoError(t, err) + + require.WithinDuration(t, now.UTC().Add(10*time.Minute), expiresTime.UTC(), time.Minute) +} + +var chdirSet = false + +func setUpTests(t require.TestingT) func() { + return chdirInPath(t, "../../../shared/pages") +} + +func chdirInPath(t require.TestingT, path string) func() { + noOp := func() {} + if chdirSet { + return noOp + } + + cwd, err := os.Getwd() + require.NoError(t, err, "Cannot Getwd") + + err = os.Chdir(path) + require.NoError(t, err, "Cannot Chdir") + + chdirSet = true + return func() { + err := os.Chdir(cwd) + require.NoError(t, err, "Cannot Chdir in cleanup") + + chdirSet = false + } +} |