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")
}
}