package main import ( "io" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "path/filepath" "strings" "testing" ) const testToken = "e2e-test-token" func newTestServer(t *testing.T) (*httptest.Server, *Store) { t.Helper() store := &Store{Root: t.TempDir()} srv := &Server{Store: store, Token: testToken} ts := httptest.NewServer(srv.handler()) t.Cleanup(ts.Close) return ts, store } // gitCmd runs git isolated from the developer's global config and credentials. func gitCmd(t *testing.T, dir string, args ...string) (string, error) { t.Helper() base := []string{"-c", "user.name=e2e", "-c", "user.email=e2e@test", "-c", "init.defaultBranch=master"} cmd := exec.Command("git", append(base, args...)...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ASKPASS=/usr/bin/false", ) out, err := cmd.CombinedOutput() return string(out), err } func mustGit(t *testing.T, dir string, args ...string) string { t.Helper() out, err := gitCmd(t, dir, args...) if err != nil { t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) } return out } // withToken embeds the owner token as basic-auth credentials in a clone URL. func withToken(t *testing.T, raw string) string { t.Helper() u, err := url.Parse(raw) if err != nil { t.Fatal(err) } u.User = url.UserPassword("owner", testToken) return u.String() } func get(t *testing.T, url string) (int, string) { t.Helper() resp, err := http.Get(url) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(body) } func TestCloneAndPushRoundtrip(t *testing.T) { ts, store := newTestServer(t) if err := store.create("demo", "a test repository"); err != nil { t.Fatal(err) } work := t.TempDir() mustGit(t, work, "clone", ts.URL+"/demo.git", "clone1") clone1 := filepath.Join(work, "clone1") if err := os.WriteFile(filepath.Join(clone1, "hello.txt"), []byte("hello from frieren\n"), 0o644); err != nil { t.Fatal(err) } mustGit(t, clone1, "add", "hello.txt") mustGit(t, clone1, "commit", "-m", "add hello") // Push without the token must be rejected. if out, err := gitCmd(t, clone1, "push", "origin", "master"); err == nil { t.Fatalf("anonymous push succeeded, want auth failure:\n%s", out) } // Push with the token must land. mustGit(t, clone1, "push", withToken(t, ts.URL+"/demo.git"), "master") // A fresh anonymous clone sees the pushed content. mustGit(t, work, "clone", ts.URL+"/demo.git", "clone2") got, err := os.ReadFile(filepath.Join(work, "clone2", "hello.txt")) if err != nil { t.Fatal(err) } if string(got) != "hello from frieren\n" { t.Fatalf("clone content = %q", got) } } func TestPushToCreate(t *testing.T) { ts, store := newTestServer(t) work := t.TempDir() mustGit(t, work, "init", "fresh") dir := filepath.Join(work, "fresh") if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644); err != nil { t.Fatal(err) } mustGit(t, dir, "add", ".") mustGit(t, dir, "commit", "-m", "first") // Creating a repo anonymously must fail. if out, err := gitCmd(t, dir, "push", ts.URL+"/newrepo.git", "master"); err == nil { t.Fatalf("anonymous push-to-create succeeded:\n%s", out) } if store.exists("newrepo") { t.Fatal("repository created by unauthenticated push") } // With the token the repository springs into existence. mustGit(t, dir, "push", withToken(t, ts.URL+"/newrepo.git"), "master") if !store.exists("newrepo") { t.Fatal("push-to-create did not create the repository") } // Invalid names never become directories. if out, err := gitCmd(t, dir, "push", withToken(t, ts.URL+"/.hidden.git"), "master"); err == nil { t.Fatalf("push to invalid repo name succeeded:\n%s", out) } } func TestWebPages(t *testing.T) { ts, store := newTestServer(t) if err := store.create("site", "web smoke test"); err != nil { t.Fatal(err) } work := t.TempDir() mustGit(t, work, "clone", ts.URL+"/site.git", "w") dir := filepath.Join(work, "w") os.MkdirAll(filepath.Join(dir, "docs"), 0o755) os.WriteFile(filepath.Join(dir, "README.md"), []byte("# site\nreadme body here\n"), 0o644) os.WriteFile(filepath.Join(dir, "docs", "guide.txt"), []byte("guide line one\n"), 0o644) os.WriteFile(filepath.Join(dir, "logo.svg"), []byte("\n"), 0o644) mustGit(t, dir, "add", ".") mustGit(t, dir, "commit", "-m", "add docs") mustGit(t, dir, "push", withToken(t, ts.URL+"/site.git"), "master") for _, tc := range []struct{ path, want string }{ {"/", "site"}, {"/site", "readme body here"}, {"/site/tree/master/docs", "guide.txt"}, {"/site/blob/master/docs/guide.txt", "guide line one"}, {"/site/raw/master/docs/guide.txt", "guide line one"}, {"/site/commits", "add docs"}, {"/site/refs", "master"}, } { code, body := get(t, ts.URL+tc.path) if code != http.StatusOK { t.Errorf("GET %s = %d", tc.path, code) continue } if !strings.Contains(body, tc.want) { t.Errorf("GET %s: missing %q", tc.path, tc.want) } } // Commit page renders the diff. commits, err := store.log(t.Context(), "site", "master", 10) if err != nil || len(commits) == 0 { t.Fatalf("log: %v", err) } code, body := get(t, ts.URL+"/site/commit/"+commits[0].Hash) if code != http.StatusOK || !strings.Contains(body, "guide line one") { t.Errorf("commit page: code %d, diff shown: %v", code, strings.Contains(body, "guide line one")) } // Media files get their real content type so README embeds render. resp, err := http.Get(ts.URL + "/site/raw/master/logo.svg") if err != nil { t.Fatal(err) } resp.Body.Close() if ct := resp.Header.Get("Content-Type"); ct != "image/svg+xml" { t.Errorf("svg raw content-type = %q", ct) } // Traversal and junk stay 404. for _, path := range []string{"/nope", "/site/blob/master/../../etc/passwd", "/site/raw/master/%2e%2e/x"} { if code, _ := get(t, ts.URL+path); code != http.StatusNotFound { t.Errorf("GET %s = %d, want 404", path, code) } } } func TestReadOnlyWithoutToken(t *testing.T) { store := &Store{Root: t.TempDir()} srv := &Server{Store: store, Token: ""} ts := httptest.NewServer(srv.handler()) defer ts.Close() work := t.TempDir() mustGit(t, work, "init", "r") dir := filepath.Join(work, "r") os.WriteFile(filepath.Join(dir, "x"), []byte("x"), 0o644) mustGit(t, dir, "add", ".") mustGit(t, dir, "commit", "-m", "x") // Even a well-formed credential is refused when no token is configured. u, _ := url.Parse(ts.URL + "/r.git") u.User = url.UserPassword("owner", "anything") if out, err := gitCmd(t, dir, "push", u.String(), "master"); err == nil { t.Fatalf("push succeeded on tokenless server:\n%s", out) } }