internal/httpd/mrpage_test.go

88fc476788ab9c640649427d815217360569b4d9
gitbay/internal/httpd/mrpage_test.go history · blame · raw

133 lines · 4528 bytes

  1package httpd
  2
  3import (
  4	"html/template"
  5	"strings"
  6	"testing"
  7	"time"
  8
  9	"gitbay.org/gitbay/internal/gitutil"
 10	"gitbay.org/gitbay/internal/store"
 11	"gitbay.org/gitbay/internal/web"
 12)
 13
 14// mrPageData mirrors the anonymous struct the mr handler renders with.
 15type mrPageData struct {
 16	repoPage
 17	MR              store.MR
 18	View            string
 19	BodyHTML        template.HTML
 20	Checks          []store.Check
 21	Combined        string
 22	Comments        []renderedComment
 23	Reviews         []reviewRow
 24	DiffFiles       []diffFile
 25	Stat            diffStat
 26	Commits         []struct{}
 27	Branches        []gitutil.Ref
 28	CanEdit         bool
 29	CanWrite        bool
 30	Unresolved      int
 31	Revisions       []store.MRHead
 32	Notice          string
 33	DetachedThreads []diffThread
 34}
 35
 36func renderMR(t *testing.T, m store.MR, reviews []store.MRReview, checks []store.Check) string {
 37	rows := make([]reviewRow, 0, len(reviews))
 38	for _, r := range reviews {
 39		// The page test renders reviews that count; whether a given
 40		// reviewer's does is decided by access, which e2e covers.
 41		rows = append(rows, reviewRow{MRReview: r, Counts: true})
 42	}
 43	t.Helper()
 44	var sb strings.Builder
 45	if err := web.Render(&sb, "mr.html", mrPageData{
 46		repoPage: testRepoPage(), MR: m, View: "conversation",
 47		Reviews: rows, Checks: checks, Combined: "",
 48	}); err != nil {
 49		t.Fatalf("render: %v", err)
 50	}
 51	return sb.String()
 52}
 53
 54func testMR(state string) store.MR {
 55	return store.MR{Number: 42, Title: "org native rendering", Author: "cmc", State: state,
 56		SourcePath: "krz/hutch", SourceRef: "org-native-rendering", TargetRef: "main",
 57		HeadSHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b"}
 58}
 59
 60// The header states what happened to the MR. "wants to merge" is only true
 61// while it is still open.
 62func TestMRHeaderByState(t *testing.T) {
 63	open := renderMR(t, testMR("open"), nil, nil)
 64	if !strings.Contains(open, "wants to merge") {
 65		t.Errorf("open MR does not say wants to merge:\n%s", open)
 66	}
 67
 68	merged := testMR("merged")
 69	merged.MergedAt, merged.MergedBy = "2026-08-27T14:03:11.000Z", "cmc"
 70	out := renderMR(t, merged, nil, nil)
 71	for _, want := range []string{"merged", "krz/hutch:org-native-rendering", "2026-08-27 14:03"} {
 72		if !strings.Contains(out, want) {
 73			t.Errorf("merged header missing %q:\n%s", want, out)
 74		}
 75	}
 76	if strings.Contains(out, "wants to merge") {
 77		t.Errorf("merged MR still wants to merge:\n%s", out)
 78	}
 79
 80	closed := testMR("closed")
 81	closed.ClosedAt, closed.ClosedBy = "2026-08-27T14:03:11.000Z", "cmc"
 82	out = renderMR(t, closed, nil, nil)
 83	if !strings.Contains(out, "without merging") || strings.Contains(out, "wants to merge") {
 84		t.Errorf("closed header:\n%s", out)
 85	}
 86
 87	// Imports and pre-0029 merges carry no stamp; the wording drops the
 88	// claim rather than inventing a time.
 89	out = renderMR(t, testMR("merged"), nil, nil)
 90	if strings.Contains(out, "wants to merge") || strings.Contains(out, " on 20") {
 91		t.Errorf("unstamped merged header:\n%s", out)
 92	}
 93}
 94
 95// Approvals and checks carry their times in the aside, so reading the MR
 96// does not mean opening the build.
 97func TestMRAsideTimestamps(t *testing.T) {
 98	out := renderMR(t, testMR("open"),
 99		[]store.MRReview{{Reviewer: "cmc", Verdict: "approve", CreatedAt: "2026-08-27T14:03:11.000Z"}},
100		[]store.Check{
101			{CommitStatus: store.CommitStatus{Context: "ci/test", State: "success",
102				UpdatedAt: "2026-08-27T14:05:00.000Z"}, Duration: 72 * time.Second, Build: 60},
103			{CommitStatus: store.CommitStatus{Context: "external/lint", State: "success",
104				UpdatedAt: "2026-08-27T14:06:00.000Z"}},
105		})
106	for _, want := range []string{"2026-08-27 14:03", "2026-08-27 14:05", "1m12s", "2026-08-27 14:06"} {
107		if !strings.Contains(out, want) {
108			t.Errorf("aside missing %q:\n%s", want, out)
109		}
110	}
111}
112
113// A job a path filter excluded has no build behind its status: the page
114// must show it as skipped rather than linking to a build that never ran
115// (#172).
116func TestMRChecksRenderSkippedWithoutBuildLink(t *testing.T) {
117	out := renderMR(t, testMR("open"), nil, []store.Check{
118		{CommitStatus: store.CommitStatus{Context: "ci/unit", State: "skipped",
119			Description: "every changed file matched paths-ignore", UpdatedAt: "2026-08-27T14:05:00.000Z"}},
120	})
121	row := ""
122	for _, line := range strings.Split(out, "\n") {
123		if strings.Contains(line, "ci/unit") {
124			row = line
125		}
126	}
127	if row == "" || !strings.Contains(row, ">skipped<") {
128		t.Fatalf("skipped check not rendered:\n%s", out)
129	}
130	if strings.Contains(row, "<a href") {
131		t.Errorf("skipped check with no build linked anyway: %s", row)
132	}
133}