git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / server / git_test.go /
internal/server/git_test.go · 7.1 KiB · raw
view: SourceMarkdown ☑ force

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, "<h1") {
		t.Fatal("?render=markdown should render markdown (expected <h1>)")
	}
}

// Non-markdown file offers only the default; force exposes & runs markdown.
{
	_, src := get("/u@d.com/demo.git/blob/main/main.go")
	if strings.Contains(src, "?render=markdown\"") {
		t.Fatal("main.go should not offer markdown without force")
	}
	if !strings.Contains(src, "force=1") {
		t.Fatal("main.go should offer a force toggle")
	}
	code, _ := get("/u@d.com/demo.git/blob/main/main.go?render=markdown&force=1")
	if code != 200 {
		t.Fatalf("forced markdown on main.go: code %d", code)
	}
}

// CSP header is present on git pages. Scripts are restricted to same-origin
// with no 'unsafe-inline', so markdown-injected inline script cannot run
// (mermaid is served as a same-origin file).
resp, _ := http.Get(ts.URL + "/u@d.com/demo.git/")
resp.Body.Close()
csp := resp.Header.Get("Content-Security-Policy")
if !strings.Contains(csp, "script-src 'self'") || strings.Contains(csp, "script-src 'self' 'unsafe-inline'") {
	t.Fatalf("missing/weak script CSP on git page: %q", csp)
}

// Commit page renders a diff.
_, logBody := get("/u@d.com/demo.git/log/feature/x")
i := strings.Index(logBody, "/u@d.com/demo.git/commit/")
if i < 0 {
	t.Fatal("no commit link in log")
}
url := logBody[i : i+len("/u@d.com/demo.git/commit/")+40]
code, body := get(url)
if code != 200 || !strings.Contains(body, "diff-add") || !strings.Contains(body, "diff-del") {
	t.Fatalf("commit page: code %d, diff classes present=%v", code,
		strings.Contains(body, "diff-add"))
}

}