Commit ba9cd01976

ba9cd01976a5dec323b1e137a47d3d0582484720

parent: d775997ead

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-11 15:53 UTC

e2e: org labels, milestones and a cross-repository close

Closes #203
e2e/orglabels_test.go added +117
@@ -0,0 +1,117 @@
1package e2e
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10// An org's labels and milestones reach every repository under it; a
11// commit in one repository closes an issue in another; the org pages
12// answer members and outsiders as their access allows.
13func TestOrgLabelsMilestonesAndCrossRepoCloses(t *testing.T) {
14 inst := startInstance(t)
15 aliceKey := inst.newKey(t, "alice")
16 carolKey := inst.newKey(t, "carol")
17 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
18 inst.admin(t, "admin", "user", "create", "carol", "--key", carolKey+".pub", "--email", "carol@example.test", "--verified")
19 must := func(key string, args ...string) string {
20 t.Helper()
21 out, errOut, code := inst.ssh(t, key, "", args...)
22 if code != 0 {
23 t.Fatalf("%v: exit %d %s", args, code, errOut)
24 }
25 return out
26 }
27 must(aliceKey, "org", "create", "acme")
28 must(aliceKey, "repo", "create", "acme/lib")
29 must(aliceKey, "repo", "create", "acme/widget", "--private")
30 must(aliceKey, "issue", "create", "acme/lib", "--title", "'lib one'")
31 must(aliceKey, "issue", "create", "acme/widget", "--title", "'widget one'")
32
33 // Repo labels in both, then the org set folds them in.
34 must(aliceKey, "issue", "label", "acme/lib", "1", "--add", "bug")
35 must(aliceKey, "issue", "label", "acme/widget", "1", "--add", "bug")
36 out := must(aliceKey, "org", "label", "set", "acme", "bug", "--color", "ff0000", "--json")
37 if !strings.Contains(out, `"folded":2`) {
38 t.Fatalf("org label set: %s", out)
39 }
40 out = must(aliceKey, "label", "list", "acme/lib", "--json")
41 if !strings.Contains(out, `"org":true`) || !strings.Contains(out, `"issues":2`) {
42 t.Fatalf("lib label list: %s", out)
43 }
44 if _, errOut, code := inst.ssh(t, aliceKey, "", "label", "set", "acme/lib", "bug"); code == 0 || !strings.Contains(errOut, "org label set acme bug") {
45 t.Fatalf("repo label set over org name: exit %d %s", code, errOut)
46 }
47
48 // An org milestone attaches from both repositories and counts across.
49 must(aliceKey, "org", "milestone", "create", "acme", "v1", "--due", "2027-01-01")
50 must(aliceKey, "issue", "milestone", "acme/lib", "1", "v1")
51 must(aliceKey, "issue", "milestone", "acme/widget", "1", "v1")
52 out = must(aliceKey, "org", "milestone", "list", "acme", "--json")
53 if !strings.Contains(out, `"open":2`) {
54 t.Fatalf("org milestone list: %s", out)
55 }
56 out = must(aliceKey, "issue", "list", "acme/lib", "--milestone", "v1", "--json")
57 if !strings.Contains(out, `"number":1`) {
58 t.Fatalf("issue list filtered by org milestone: %s", out)
59 }
60
61 // A push to acme/lib closes acme/widget#1 and leaves a comment there.
62 work := t.TempDir()
63 env := inst.gitEnv(aliceKey)
64 mustGit(t, work, env, "clone", inst.sshURL("acme/lib"), "w")
65 dir := filepath.Join(work, "w")
66 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
67 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
68 mustGit(t, dir, env, "add", ".")
69 mustGit(t, dir, env, "commit", "-q", "-m", "fix the widget\n\nCloses acme/widget#1")
70 mustGit(t, dir, env, "push", "-q", "origin", "main")
71 out = must(aliceKey, "issue", "show", "acme/widget", "1", "--json")
72 if !strings.Contains(out, `"state":"closed"`) || !strings.Contains(out, "](/acme/lib/commit/") {
73 t.Fatalf("widget#1 after cross-repo close: %s", out)
74 }
75 out = must(aliceKey, "org", "milestone", "list", "acme", "--json")
76 if !strings.Contains(out, `"open":1`) || !strings.Contains(out, `"closed":1`) {
77 t.Fatalf("org milestone progress after close: %s", out)
78 }
79
80 // carol is outside: she reads the org pages because acme/lib is public,
81 // and the counts stop at it.
82 out = must(carolKey, "org", "milestone", "list", "acme", "--json")
83 if !strings.Contains(out, `"open":1`) || !strings.Contains(out, `"closed":0`) {
84 t.Fatalf("outsider progress: %s", out)
85 }
86 if status, body := inst.get(t, "/acme/-/labels"); status != 200 || !strings.Contains(body, ">bug<") {
87 t.Fatalf("org labels page: %d", status)
88 }
89 if status, body := inst.get(t, "/acme/-/milestones"); status != 200 || !strings.Contains(body, "v1") || !strings.Contains(body, "0 closed, 1 open") {
90 t.Fatalf("org milestones page: %d\n%s", status, body)
91 }
92 if status, body := inst.get(t, "/acme/lib/labels"); status != 200 || !strings.Contains(body, `chip-neutral">org<`) {
93 t.Fatalf("repo labels page lacks the org mark: %d", status)
94 }
95 // carol cannot close into the private repo from a repo she owns.
96 must(carolKey, "repo", "create", "carol/own")
97 must(aliceKey, "issue", "create", "acme/widget", "--title", "'widget two'")
98 cwork := t.TempDir()
99 cenv := inst.gitEnv(carolKey)
100 mustGit(t, cwork, cenv, "clone", inst.sshURL("carol/own"), "w")
101 cdir := filepath.Join(cwork, "w")
102 os.WriteFile(filepath.Join(cdir, "a.txt"), []byte("a\n"), 0o644)
103 mustGit(t, cdir, cenv, "checkout", "-q", "-b", "main")
104 mustGit(t, cdir, cenv, "add", ".")
105 mustGit(t, cdir, cenv, "commit", "-q", "-m", "sneaky\n\nCloses acme/widget#2")
106 mustGit(t, cdir, cenv, "push", "-q", "origin", "main")
107 out = must(aliceKey, "issue", "show", "acme/widget", "2", "--json")
108 if !strings.Contains(out, `"state":"open"`) || strings.Contains(out, "sneaky") {
109 t.Fatalf("outsider acted on a private repo's issue: %s", out)
110 }
111 // With the public repo gone private, the org pages are not found for
112 // an anonymous reader.
113 must(aliceKey, "repo", "settings", "visibility", "acme/lib", "private")
114 if status, _ := inst.get(t, "/acme/-/labels"); status != 404 {
115 t.Fatalf("private org labels page for anonymous: %d", status)
116 }
117}