package main import ( "crypto/tls" "fmt" "io/ioutil" "mime" "net" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/namsral/flag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var pagesBinary = flag.String("gitlab-pages-binary", "./gitlab-pages", "Path to the gitlab-pages binary") // TODO: Use TCP port 0 everywhere to avoid conflicts. The binary could output // the actual port (and type of listener) for us to read in place of the // hardcoded values below. var listeners = []ListenSpec{ {"http", "127.0.0.1", "37000"}, {"http", "::1", "37000"}, {"https", "127.0.0.1", "37001"}, {"https", "::1", "37001"}, {"proxy", "127.0.0.1", "37002"}, {"proxy", "::1", "37002"}, } var ( httpListener = listeners[0] httpsListener = listeners[2] ) func skipUnlessEnabled(t *testing.T, conditions ...string) { if testing.Short() { t.Log("Acceptance tests disabled") t.SkipNow() } if _, err := os.Stat(*pagesBinary); os.IsNotExist(err) { t.Errorf("Couldn't find gitlab-pages binary at %s", *pagesBinary) t.FailNow() } for _, condition := range conditions { switch condition { case "not-inplace-chroot": if os.Getenv("TEST_DAEMONIZE") == "inplace" { t.Log("Not supported with -daemon-inplace-chroot") t.SkipNow() } default: t.Error("Unknown condition:", condition) t.FailNow() } } } func TestUnknownHostReturnsNotFound(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() for _, spec := range listeners { rsp, err := GetPageFromListener(t, spec, "invalid.invalid", "") require.NoError(t, err) rsp.Body.Close() assert.Equal(t, http.StatusNotFound, rsp.StatusCode) } } func TestUnknownProjectReturnsNotFound(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "/nonexistent/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusNotFound, rsp.StatusCode) } func TestGroupDomainReturns200(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestKnownHostReturns200(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() for _, spec := range listeners { rsp, err := GetPageFromListener(t, spec, "group.gitlab-example.com", "project/") require.NoError(t, err) rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } } func TestCORSWhenDisabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-disable-cross-origin-requests") defer teardown() for _, spec := range listeners { for _, method := range []string{"GET", "OPTIONS"} { rsp := doCrossOriginRequest(t, method, method, spec.URL("project/")) assert.Equal(t, http.StatusOK, rsp.StatusCode) assert.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Origin")) assert.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Credentials")) } } } func TestCORSAllowsGET(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() for _, spec := range listeners { for _, method := range []string{"GET", "OPTIONS"} { rsp := doCrossOriginRequest(t, method, method, spec.URL("project/")) assert.Equal(t, http.StatusOK, rsp.StatusCode) assert.Equal(t, "*", rsp.Header.Get("Access-Control-Allow-Origin")) assert.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Credentials")) } } } func TestCORSForbidsPOST(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() for _, spec := range listeners { rsp := doCrossOriginRequest(t, "OPTIONS", "POST", spec.URL("project/")) assert.Equal(t, http.StatusOK, rsp.StatusCode) assert.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Origin")) assert.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Credentials")) } } func doCrossOriginRequest(t *testing.T, method, reqMethod, url string) *http.Response { req, err := http.NewRequest(method, url, nil) require.NoError(t, err) req.Host = "group.gitlab-example.com" req.Header.Add("Origin", "example.com") req.Header.Add("Access-Control-Request-Method", reqMethod) var rsp *http.Response err = fmt.Errorf("no request was made") for start := time.Now(); time.Since(start) < 1*time.Second; { rsp, err = DoPagesRequest(t, req) if err == nil { break } time.Sleep(100 * time.Millisecond) } require.NoError(t, err) rsp.Body.Close() return rsp } func TestKnownHostWithPortReturns200(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() for _, spec := range listeners { rsp, err := GetPageFromListener(t, spec, "group.gitlab-example.com:"+spec.Port, "project/") require.NoError(t, err) rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } } func TestHttpToHttpsRedirectDisabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "project/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) rsp, err = GetPageFromListener(t, httpsListener, "group.gitlab-example.com", "project/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestHttpToHttpsRedirectEnabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-redirect-http=true") defer teardown() rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "project/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusTemporaryRedirect, rsp.StatusCode) assert.Equal(t, 1, len(rsp.Header["Location"])) assert.Equal(t, "https://group.gitlab-example.com/project/", rsp.Header.Get("Location")) rsp, err = GetPageFromListener(t, httpsListener, "group.gitlab-example.com", "project/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestHttpsOnlyGroupEnabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetRedirectPage(t, httpListener, "group.https-only.gitlab-example.com", "project1/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusMovedPermanently, rsp.StatusCode) } func TestHttpsOnlyGroupDisabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetPageFromListener(t, httpListener, "group.https-only.gitlab-example.com", "project2/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestHttpsOnlyProjectEnabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetRedirectPage(t, httpListener, "test.my-domain.com", "/index.html") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusMovedPermanently, rsp.StatusCode) } func TestHttpsOnlyProjectDisabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetPageFromListener(t, httpListener, "test2.my-domain.com", "/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestHttpsOnlyDomainDisabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetPageFromListener(t, httpListener, "no.cert.com", "/") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestPrometheusMetricsCanBeScraped(t *testing.T) { skipUnlessEnabled(t) listener := []ListenSpec{{"http", "127.0.0.1", "37003"}} teardown := RunPagesProcess(t, *pagesBinary, listener, ":42345") defer teardown() resp, err := http.Get("http://localhost:42345/metrics") if assert.NoError(t, err) { defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) assert.Contains(t, string(body), "gitlab_pages_http_sessions_active 0") assert.Contains(t, string(body), "gitlab_pages_domains_served_total 11") } } func TestStatusPage(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-pages-status=/@statuscheck") defer teardown() rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "@statuscheck") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestStatusNotYetReady(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcessWithoutWait(t, *pagesBinary, listeners, "", "-pages-status=/@statuscheck", "-pages-root=shared/invalid-pages") defer teardown() waitForRoundtrips(t, listeners, 5*time.Second) rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "@statuscheck") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusServiceUnavailable, rsp.StatusCode) } func TestPageNotAvailableIfNotLoaded(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcessWithoutWait(t, *pagesBinary, listeners, "", "-pages-root=shared/invalid-pages") defer teardown() waitForRoundtrips(t, listeners, 5*time.Second) rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "index.html") require.NoError(t, err) defer rsp.Body.Close() assert.Equal(t, http.StatusServiceUnavailable, rsp.StatusCode) } func TestObscureMIMEType(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcessWithoutWait(t, *pagesBinary, listeners, "") defer teardown() require.NoError(t, httpListener.WaitUntilRequestSucceeds(nil)) rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "project/file.webmanifest") require.NoError(t, err) defer rsp.Body.Close() require.Equal(t, http.StatusOK, rsp.StatusCode) mt, _, err := mime.ParseMediaType(rsp.Header.Get("Content-Type")) require.NoError(t, err) assert.Equal(t, "application/manifest+json", mt) } func TestArtifactProxyRequest(t *testing.T) { skipUnlessEnabled(t, "not-inplace-chroot") transport := (TestHTTPSClient.Transport).(*http.Transport) defer func(t time.Duration) { transport.ResponseHeaderTimeout = t }(transport.ResponseHeaderTimeout) transport.ResponseHeaderTimeout = 5 * time.Second content := "