package web
import (
"context"
"io"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/ops"
)
// A private repository does not exist for anyone who may not read it:
// every route under it answers exactly what an unknown repository
// answers. The redirects and the input checks used to run before the
// access check, so a signed-out visitor could tell the two apart by a
// 303, a 307 or a 400 (295).
func TestPrivateRepositoryAnswersLikeANonexistentOne(t *testing.T) {
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v",
Repos: api.NewRepos(map[string]*api.Repo{"orchard": {Index: landingIndex(t, false)}, "pear": {Index: landingIndex(t, false)}}),
// As production's repoFor: a private repository is readable
// only by a signed-in viewer.
CanRead: func(ctx context.Context, repo string) bool { return ops.User(ctx) != "" }}); err != nil {
t.Fatal(err)
}
plane := &memCaps{stored: map[string]string{"dan": "a"}}
srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"orchard": ops.Admin}}),
WithCaps(plane),
WithNamespaces(place(map[string]string{"orchard": "grove", "pear": ""})),
WithPrivateRepos(func(repo string) bool { return repo == "orchard" || repo == "pear" }),
}))
t.Cleanup(srv.Close)
get := func(path string, signedIn bool) (int, string, string, string) {
t.Helper()
req, _ := http.NewRequest("GET", srv.URL+path, nil)
if signedIn {
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
}
client := *srv.Client()
client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return resp.StatusCode, resp.Header.Get("Location"), resp.Header.Get("Content-Type"), string(b)
}
// The footer reports how long this render took, which differs per
// request and says nothing about the repository.
timing := regexp.MustCompile(`<span class="f-time" data-volatile>.*</span></span></span>`)
tails := []string{
"/doc/trunk/README.md", "/history/trunk/README.md/", "/tree/trunk/src/",
"/checkin/abc", "/checkin/../x", "/info/abc", "/attachment/abc/file.txt",
"", "/timeline", "/wiki", "/file/trunk/README.md", "/raw/trunk/README.md",
"/archive/trunk.tar.gz", "/zip/trunk.zip",
}
for _, tail := range tails {
for _, shape := range []struct{ name, repo, private, missing string }{
{"owner-qualified", "orchard", "/r/grove/orchard", "/r/grove/ghost"},
{"wrong owner", "orchard", "/r/wrong/orchard", "/r/wrong/ghost"},
{"owner-less namespaced", "orchard", "/r/orchard", "/r/ghost"},
{"owner-less flat", "pear", "/r/pear", "/r/ghost"},
} {
priv := shape.private + tail
code, loc, ctype, body := get(priv, false)
wcode, wloc, wctype, wbody := get(shape.missing+tail, false)
if code != wcode || loc != strings.ReplaceAll(wloc, "ghost", shape.repo) || ctype != wctype {
t.Errorf("FAIL %s %s: private %d loc=%q %s, nonexistent %d loc=%q %s",
shape.name, tail, code, loc, ctype, wcode, wloc, wctype)
}
if timing.ReplaceAllString(strings.ReplaceAll(wbody, "ghost", shape.repo), "") != timing.ReplaceAllString(body, "") {
t.Errorf("FAIL %s %s: the body differs from an unknown repository's beyond the name", shape.name, tail)
}
}
}
// A member still gets the normal answers.
if code, loc, _, _ := get("/r/orchard", true); code != http.StatusFound || loc != "/r/grove/orchard" {
t.Errorf("FAIL member flat root: %d -> %q", code, loc)
}
// The doc route reaches its handler (this fixture has no store, so
// it fails there rather than rendering); what matters is that it is
// not the stranger's 404.
if code, _, _, _ := get("/r/grove/orchard/doc/trunk/README.md", true); code == http.StatusNotFound {
t.Errorf("FAIL member doc route answered the stranger's 404")
}
if code, _, _, _ := get("/r/grove/orchard/timeline", true); code != http.StatusOK {
t.Errorf("FAIL member timeline: %d", code)
}
}