package httpd import ( "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" "gitbay.org/gitbay/internal/config" "gitbay.org/gitbay/internal/control" "gitbay.org/gitbay/internal/store" "gitbay.org/gitbay/internal/web" ) // The range-diff page dispatches mr range-diff and renders its text // output, the same comparison the CLI and iOS already show (#269). func TestMRRangeDiffPageRendersCommandOutput(t *testing.T) { st, err := store.Open(":memory:") if err != nil { t.Fatal(err) } defer st.Close() if err := st.MigrateUp(); err != nil { t.Fatal(err) } uid, err := st.CreateUser("alice", false) if err != nil { t.Fatal(err) } if _, err := st.CreateRepo("user", uid, "app", "public"); err != nil { t.Fatal(err) } s := New(config.Default(), st) req := httptest.NewRequest("GET", "/alice/app/mrs/1/range-diff", nil) req.SetPathValue("owner", "alice") req.SetPathValue("repo", "app") req.SetPathValue("n", "1") rr := httptest.NewRecorder() s.mrRangeDiff(rr, req) // No merge request 1 exists yet, so this must 404 rather than error. if rr.Code != 404 { t.Fatalf("status %d, body %s", rr.Code, rr.Body.String()) } } // rangeDiffGitEnv and rangeDiffGitRunner build real git history on disk, // the way internal/control's own build tests do, since mr range-diff // runs actual git commands against the repository's bare directory — a // store-only fixture cannot exercise it. func rangeDiffGitEnv() []string { return append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test") } func rangeDiffGitRunner(t *testing.T) func(dir string, args ...string) string { t.Helper() env := rangeDiffGitEnv() return func(dir string, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = env out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return string(out) } } // rangeDiffFixture builds a private repository owned by alice with a // merge request that has two real revisions on disk — an "add b" commit, // then the same commit amended with one changed line — the same shape // e2e/rangediff_test.go:14-38 builds for the CLI. bob holds no access to // the repository, so he stands in for a non-reader. func rangeDiffFixture(t *testing.T) (st *store.Store, cfg config.Config, alice, bob store.User, repo store.Repo, n int64, v1, v2, title string) { t.Helper() root := t.TempDir() st, err := store.Open(filepath.Join(root, "gitbay.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) if err := st.MigrateUp(); err != nil { t.Fatal(err) } aliceID, err := st.CreateUser("alice", false) if err != nil { t.Fatal(err) } bobID, err := st.CreateUser("bob", false) if err != nil { t.Fatal(err) } repoID, err := st.CreateRepo("user", aliceID, "secret", "private") if err != nil { t.Fatal(err) } repo, err = st.RepoByID(repoID) if err != nil { t.Fatal(err) } git := rangeDiffGitRunner(t) dir := control.RepoDir(root, repo.OwnerName, repo.Name) if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil { t.Fatal(err) } git(root, "init", "-q", "--bare", dir) src := filepath.Join(root, "src") git(root, "init", "-q", "-b", "main", "src") os.WriteFile(filepath.Join(src, "a.txt"), []byte("one\n"), 0o644) git(src, "add", ".") git(src, "commit", "-q", "-m", "base") git(src, "push", "-q", dir, "main") git(src, "checkout", "-q", "-b", "feat") os.WriteFile(filepath.Join(src, "b.txt"), []byte("alpha\nbeta\ngamma\n"), 0o644) git(src, "add", ".") git(src, "commit", "-q", "-m", "add b") git(src, "push", "-q", dir, "feat") v1 = strings.TrimSpace(git(src, "rev-parse", "HEAD")) os.WriteFile(filepath.Join(src, "b.txt"), []byte("alpha\nbeta revised\ngamma\n"), 0o644) git(src, "add", ".") git(src, "commit", "-q", "--amend", "--no-edit") git(src, "push", "-q", "--force", dir, "feat") v2 = strings.TrimSpace(git(src, "rev-parse", "HEAD")) title = "range diff of a secret plan" n, err = st.CreateMR(repo.ID, aliceID, repo.ID, "feat", "main", title, "", v1, "md", false) if err != nil { t.Fatal(err) } mr, err := st.MRByNumber(repo.ID, n) if err != nil { t.Fatal(err) } if err := st.UpdateMRHead(mr.ID, v2, "", false); err != nil { t.Fatal(err) } cfg = config.Default() cfg.Server.Root = root cfg.Web.Mode = "accounts" alice = store.User{ID: aliceID, Username: "alice"} bob = store.User{ID: bobID, Username: "bob"} return } // loginCookie mints a real web session, the same as a browser login // would, so a handler under test reads a viewer through s.viewer / // s.webViewer exactly as it does in production. func loginCookie(t *testing.T, st *store.Store, userID int64) *http.Cookie { t.Helper() tok, hash, err := store.NewToken() if err != nil { t.Fatal(err) } if err := st.CreateWebSession(hash, userID, time.Hour); err != nil { t.Fatal(err) } return &http.Cookie{Name: sessionCookie, Value: tok} } // The range-diff page must answer exactly as the MR page does: the owner // reads it, and a private repository is 404 — never 403, which would // confirm the namespace — for both an anonymous caller and a signed-in // user with no access (#269). func TestMRRangeDiffPagePrivateRepo(t *testing.T) { st, cfg, alice, bob, repo, n, _, _, title := rangeDiffFixture(t) s := New(cfg, st) newReq := func(cookie *http.Cookie) (*httptest.ResponseRecorder, *http.Request) { req := httptest.NewRequest("GET", "/alice/secret/mrs/"+strconv.FormatInt(n, 10)+"/range-diff", nil) req.SetPathValue("owner", repo.OwnerName) req.SetPathValue("repo", repo.Name) req.SetPathValue("n", strconv.FormatInt(n, 10)) if cookie != nil { req.AddCookie(cookie) } return httptest.NewRecorder(), req } t.Run("owner reads it", func(t *testing.T) { rr, req := newReq(loginCookie(t, st, alice.ID)) s.mrRangeDiff(rr, req) if rr.Code != 200 { t.Fatalf("status %d, body %s", rr.Code, rr.Body.String()) } if !strings.Contains(rr.Body.String(), title) { t.Errorf("owner's page does not show the MR title %q:\n%s", title, rr.Body.String()) } }) t.Run("anonymous gets 404 and no title", func(t *testing.T) { rr, req := newReq(nil) s.mrRangeDiff(rr, req) if rr.Code != 404 { t.Fatalf("status %d, want 404 (never 403), body %s", rr.Code, rr.Body.String()) } if strings.Contains(rr.Body.String(), title) { t.Errorf("404 body leaks the MR title %q:\n%s", title, rr.Body.String()) } }) t.Run("a signed-in non-reader gets 404 and no title", func(t *testing.T) { rr, req := newReq(loginCookie(t, st, bob.ID)) s.mrRangeDiff(rr, req) if rr.Code != 404 { t.Fatalf("status %d, want 404 (never 403), body %s", rr.Code, rr.Body.String()) } if strings.Contains(rr.Body.String(), title) { t.Errorf("404 body leaks the MR title %q:\n%s", title, rr.Body.String()) } }) } // --from/--to reach the control command as real argv: an unknown // revision is refused with not-found, and two real revisions render the // range-diff between exactly those two. func TestMRRangeDiffPageFromToQuery(t *testing.T) { st, cfg, alice, _, repo, n, v1, v2, _ := rangeDiffFixture(t) s := New(cfg, st) cookie := loginCookie(t, st, alice.ID) newReq := func(query string) (*httptest.ResponseRecorder, *http.Request) { req := httptest.NewRequest("GET", "/alice/secret/mrs/"+strconv.FormatInt(n, 10)+"/range-diff"+query, nil) req.SetPathValue("owner", repo.OwnerName) req.SetPathValue("repo", repo.Name) req.SetPathValue("n", strconv.FormatInt(n, 10)) req.AddCookie(cookie) return httptest.NewRecorder(), req } t.Run("unknown revision is 404", func(t *testing.T) { rr, req := newReq("?from=0000000000000000000000000000000000000000") s.mrRangeDiff(rr, req) if rr.Code != 404 { t.Fatalf("status %d, want 404, body %s", rr.Code, rr.Body.String()) } }) t.Run("two real revisions render the diff between them", func(t *testing.T) { rr, req := newReq("?from=" + v1 + "&to=" + v2) s.mrRangeDiff(rr, req) if rr.Code != 200 { t.Fatalf("status %d, body %s", rr.Code, rr.Body.String()) } body := rr.Body.String() if !strings.Contains(body, "beta revised") { t.Errorf("range-diff does not show the changed line:\n%s", body) } }) t.Run("the same revision twice is a usage error rendered inline", func(t *testing.T) { rr, req := newReq("?from=" + v1 + "&to=" + v1) s.mrRangeDiff(rr, req) if rr.Code != 200 { t.Fatalf("status %d, want 200 (the error renders on the page), body %s", rr.Code, rr.Body.String()) } if !strings.Contains(rr.Body.String(), "same revision") { t.Errorf("page does not show the usage error:\n%s", rr.Body.String()) } }) } // mrRangeDiffPageData mirrors the anonymous struct mrRangeDiff renders // with, the way mrPageData mirrors mr's in mrpage_test.go:16-40. type mrRangeDiffPageData struct { repoPage MR store.MR Diff string Error string } func testRangeDiffMR() store.MR { return store.MR{Number: 7, Title: "org native rendering", Author: "cmc"} } // The diff branch renders the command's raw text output, HTML-escaped — // it is not markup, and must not be treated as any. func TestMRRangeDiffTemplateEscapesDiff(t *testing.T) { var sb strings.Builder if err := web.Render(&sb, "mrrangediff.html", mrRangeDiffPageData{ repoPage: testRepoPage(), MR: testRangeDiffMR(), Diff: ``, }); err != nil { t.Fatalf("render: %v", err) } out := sb.String() if strings.Contains(out, "