package server import ( "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "gitweb/internal/store" ) // newTestServer builds a Server over a LocalStore namespace containing a bare // repo at u@d.com/demo.git and a plain dir u@d.com/docs. func newTestServer(t *testing.T) (*httptest.Server, func()) { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } ns := t.TempDir() if err := os.MkdirAll(filepath.Join(ns, "u@d.com", "docs"), 0o755); err != nil { t.Fatal(err) } os.WriteFile(filepath.Join(ns, "u@d.com", "docs", "note.txt"), []byte("hi"), 0o644) // Browsing is gated by Access files; make this whole namespace world-readable // so the anonymous browse tests below succeed. os.WriteFile(filepath.Join(ns, "u@d.com", "Access"), []byte("read,list: all\n*: u@d.com\n"), 0o644) work := t.TempDir() git := func(dir string, a ...string) { t.Helper() c := exec.Command("git", a...) c.Dir = dir c.Env = append(os.Environ(), "GIT_AUTHOR_NAME=T", "GIT_AUTHOR_EMAIL=t@e.com", "GIT_COMMITTER_NAME=T", "GIT_COMMITTER_EMAIL=t@e.com") if out, err := c.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", a, err, out) } } git(work, "init", "-q", "-b", "main") os.WriteFile(filepath.Join(work, "README.md"), []byte("# Demo\n\nhello\n"), 0o644) os.WriteFile(filepath.Join(work, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644) // An Access file committed into the repo tree, to prove it is hidden there too. os.WriteFile(filepath.Join(work, "Access"), []byte("read,list: all\n*: secret@d.com\n"), 0o644) git(work, "add", "-A") git(work, "commit", "-qm", "initial") git(work, "checkout", "-q", "-b", "feature/x") os.WriteFile(filepath.Join(work, "main.go"), []byte("package main\n\nfunc main() { _ = 1 }\n"), 0o644) git(work, "commit", "-qam", "tweak") git(work, "tag", "v1.0.0") git(work, "checkout", "-q", "main") git(work, "clone", "-q", "--bare", work, filepath.Join(ns, "u@d.com", "demo.git")) web := os.DirFS("../../web") srv, err := New(Config{Store: store.NewLocalStore(ns), Web: http.FS(web), CacheDir: t.TempDir()}) if err != nil { t.Fatal(err) } ts := httptest.NewServer(srv.Handler()) return ts, ts.Close } // TestAccessFilesHidden verifies that upspin Access files are never listed or // rendered — neither in the plain namespace browse nor in a git repo tree — so // the set of users/groups with access is not exposed to browsers. func TestAccessFilesHidden(t *testing.T) { ts, done := newTestServer(t) defer done() get := func(p string) (int, string) { resp, err := http.Get(ts.URL + p) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(b) } // Namespace listing must not link to the Access file... if code, body := get("/u@d.com/"); code != 200 { t.Fatalf("namespace browse code %d", code) } else if strings.Contains(body, `href="Access"`) { t.Error("namespace listing exposed the Access file") } // ...and its contents must not be servable directly. if code, _ := get("/u@d.com/Access"); code != 404 { t.Errorf("GET /u@d.com/Access = %d, want 404", code) } // Git tree listing must not link to a committed Access file... if code, body := get("/u@d.com/demo.git/tree/main/"); code != 200 { t.Fatalf("tree code %d", code) } else if strings.Contains(body, "/blob/main/Access") { t.Error("git tree listing exposed a committed Access file") } // ...and neither blob nor raw view will render it. if code, body := get("/u@d.com/demo.git/blob/main/Access"); code != 404 { t.Errorf("blob Access = %d, want 404", code) } else if strings.Contains(body, "secret@d.com") { t.Error("blob view leaked Access contents") } if code, _ := get("/u@d.com/demo.git/raw/main/Access"); code != 404 { t.Errorf("raw Access = %d, want 404", code) } } func TestGitVerbs(t *testing.T) { ts, done := newTestServer(t) defer done() get := func(p string) (int, string) { resp, err := http.Get(ts.URL + p) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(b) } cases := []struct { path string wantCode int contains []string }{ {"/u@d.com/docs/", 200, []string{"Index of", "note.txt"}}, {"/u@d.com/demo.git/", 200, []string{"feature/x", "v1.0.0", "README", "Demo"}}, {"/u@d.com/demo.git/refs", 200, []string{"Branches", "Tags", "feature/x", "v1.0.0"}}, {"/u@d.com/demo.git/tree/main/", 200, []string{"main.go", "README.md"}}, {"/u@d.com/demo.git/blob/main/main.go", 200, []string{"chroma", "func"}}, {"/u@d.com/demo.git/blob/feature/x/main.go", 200, []string{"chroma"}}, {"/u@d.com/demo.git/log/feature/x", 200, []string{"tweak", "/commit/"}}, {"/u@d.com/demo.git/bogus", 404, nil}, } for _, c := range cases { t.Run(c.path, func(t *testing.T) { code, body := get(c.path) if code != c.wantCode { t.Fatalf("code = %d, want %d", code, c.wantCode) } for _, want := range c.contains { if !strings.Contains(body, want) { t.Fatalf("body missing %q", want) } } }) } // Renderer switch: default README.md is highlighted source, with a // Markdown switch offered; ?render=markdown renders the markdown. { _, src := get("/u@d.com/demo.git/blob/main/README.md") if !strings.Contains(src, "chroma") { t.Fatal("README.md default view should be highlighted source") } if !strings.Contains(src, `href="/u@d.com/demo.git/blob/main/README.md?render=markdown"`) { t.Fatal("README.md should offer a Markdown switch link") } _, md := get("/u@d.com/demo.git/blob/main/README.md?render=markdown") if !strings.Contains(md, "