Skip to content
quarry Artifact [22ce4ad415]
Login

Artifact [22ce4ad415]

Artifact 22ce4ad415521a0f434e5a7cf9b13098123cfa43d383b949ed8bbde8352e94d7:


package web

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/ops"
)

// A forge reached over https is not usable over http through its proxy:
// a page load is sent to https, and anything carrying a credential or a
// body is refused, not redirected, because it has already been sent.
func TestRequireHTTPSSendsPagesToHTTPSAndRefusesTheRest(t *testing.T) {
	ran := false
	h := RequireHTTPS("https://forge.example", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ran = true
	}))
	for _, tc := range []struct {
		name, method, proto string
		wantCode            int
		wantRan             bool
		wantLoc, wantHSTS   bool
	}{
		{"a page over http is redirected", "GET", "http", http.StatusPermanentRedirect, false, true, false},
		{"a HEAD over http is redirected", "HEAD", "http", http.StatusPermanentRedirect, false, true, false},
		{"a sync or form post over http is refused", "POST", "http", http.StatusForbidden, false, false, false},
		{"https is served and told to stay https", "GET", "https", http.StatusOK, true, false, true},
		{"a direct loopback request is served as before", "POST", "", http.StatusOK, true, false, false},
	} {
		ran = false
		req := httptest.NewRequest(tc.method, "/r/quarry/xfer?x=1", nil)
		if tc.proto != "" {
			req.Header.Set("X-Forwarded-Proto", tc.proto)
		}
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, req)
		loc := rec.Header().Get("Location")
		hsts := rec.Header().Get("Strict-Transport-Security") != ""
		switch {
		case rec.Code != tc.wantCode:
			t.Errorf("FAIL: %s: status %d, want %d", tc.name, rec.Code, tc.wantCode)
		case ran != tc.wantRan:
			t.Errorf("FAIL: %s: handler ran = %v, want %v", tc.name, ran, tc.wantRan)
		case tc.wantLoc && loc != "https://forge.example/r/quarry/xfer?x=1":
			t.Errorf("FAIL: %s: redirected to %q", tc.name, loc)
		case hsts != tc.wantHSTS:
			t.Errorf("FAIL: %s: HSTS present = %v, want %v", tc.name, hsts, tc.wantHSTS)
		default:
			t.Logf("ok: %s", tc.name)
		}
	}
}

// A forge that is not served over https has nothing to upgrade to.
func TestRequireHTTPSLeavesAnHTTPForgeAlone(t *testing.T) {
	ran := false
	h := RequireHTTPS("http://127.0.0.1:8977", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ran = true }))
	req := httptest.NewRequest("POST", "/login", nil)
	req.Header.Set("X-Forwarded-Proto", "http")
	h.ServeHTTP(httptest.NewRecorder(), req)
	if !ran {
		t.Error("FAIL: a forge with an http origin refused its own scheme")
	} else {
		t.Log("ok: an http forge is served over http")
	}
}

// Bytes a signed-in reader fetched may be a private repository's, and
// the edge cache in front of the forge must not keep them for the next
// stranger. Only an anonymous reader's response may be shared.
func TestOnlyAnAnonymousReadersBytesMayBeSharedByACache(t *testing.T) {
	anon := httptest.NewRequest("GET", "/r/x/doc/trunk/a.png", nil)
	user := anon.WithContext(ops.WithUser(anon.Context(), "dan"))
	basic := httptest.NewRequest("GET", "/r/x/doc/trunk/a.png", nil)
	basic.SetBasicAuth("dan", "token")
	cookie := httptest.NewRequest("GET", "/r/x/doc/trunk/a.png", nil)
	cookie.AddCookie(&http.Cookie{Name: sessionCookie, Value: "t"})
	for _, tc := range []struct {
		name   string
		req    *http.Request
		public bool
	}{
		{"an anonymous reader", anon, true},
		{"a signed-in reader", user, false},
		{"a reader with a Basic credential", basic, false},
		{"a reader with a session cookie", cookie, false},
	} {
		got := cacheFor(tc.req, 60, false)
		if strings.HasPrefix(got, "public") != tc.public {
			t.Errorf("FAIL: %s got %q", tc.name, got)
		} else {
			t.Logf("ok: %s gets %q", tc.name, got)
		}
	}

	// And the inline attachment route, which serves by hash, follows it.
	rec := httptest.NewRecorder()
	serveInlineMedia(rec, user, "a.png", "image/png", []byte("x"))
	if cc := rec.Header().Get("Cache-Control"); !strings.HasPrefix(cc, "private") {
		t.Errorf("FAIL: a signed-in reader's attachment is cached as %q", cc)
	} else {
		t.Log("ok: a signed-in reader's attachment is private")
	}
}