krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
1package e2e
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestOrganizations(t *testing.T) {
11 inst := startInstance(t)
12 aliceKey := inst.newKey(t, "alice")
13 bobKey := inst.newKey(t, "bob")
14 eveKey := inst.newKey(t, "eve")
15 inst.admin(t, "admin", "user", "create", "alice",
16 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
17 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
18 inst.admin(t, "admin", "user", "create", "eve", "--key", eveKey+".pub")
19
20 // Alice creates an org; the namespace is shared with users.
21 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "krz"); code != 0 {
22 t.Fatalf("org create: %s", errOut)
23 }
24 if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "bob"); code == 0 {
25 t.Fatal("org created with a user's name")
26 }
27 if out := inst.admin(t, "admin", "user", "create", "krz2", "--key", inst.newKey(t, "krz2")+".pub"); out == "" {
28 t.Fatal("control user create failed")
29 }
30 // A user cannot claim an org's name either.
31 cmd := inst.forgedAdminErr(t, "admin", "user", "create", "krz")
32 if !strings.Contains(cmd, "taken") {
33 t.Fatalf("user with org name: %s", cmd)
34 }
35
36 // Only org admins create repos under the org.
37 if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "create", "krz/lib"); code != 4 {
38 t.Fatalf("non-member org repo create: %d %s", code, errOut)
39 }
40 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "krz/lib", "--private"); code != 0 {
41 t.Fatalf("org repo create: %s", errOut)
42 }
43
44 // Membership-derived access: bob (member) gets write, eve (outsider)
45 // sees nothing on the private repo.
46 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "members", "add", "krz", "bob"); code != 0 {
47 t.Fatalf("members add: %s", errOut)
48 }
49 work := t.TempDir()
50 bobEnv := inst.gitEnv(bobKey)
51 mustGit(t, work, bobEnv, "clone", inst.sshURL("krz/lib"), "w")
52 dir := filepath.Join(work, "w")
53 os.WriteFile(filepath.Join(dir, "f.txt"), []byte("org work\n"), 0o644)
54 mustGit(t, dir, bobEnv, "checkout", "-q", "-b", "main")
55 mustGit(t, dir, bobEnv, "add", ".")
56 mustGit(t, dir, bobEnv, "commit", "-q", "-m", "bob pushes to org repo")
57 mustGit(t, dir, bobEnv, "push", "-q", "origin", "main")
58
59 if out, code := gitRun(t, t.TempDir(), inst.gitEnv(eveKey), "clone", inst.sshURL("krz/lib")); code == 0 || !strings.Contains(out, "repository not found") {
60 t.Fatalf("outsider on private org repo: %d\n%s", code, out)
61 }
62
63 // Members are not repo admins: bob cannot change settings or grant
64 // access; an org admin can.
65 if _, _, code := inst.ssh(t, bobKey, "", "repo", "settings", "protect", "krz/lib", "main"); code != 4 {
66 t.Fatal("member changed org repo settings")
67 }
68 if _, _, code := inst.ssh(t, bobKey, "", "org", "members", "add", "krz", "eve"); code != 4 {
69 t.Fatal("member managed org membership")
70 }
71 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "settings", "protect", "krz/lib", "main"); code != 0 {
72 t.Fatalf("org admin protect: %s", errOut)
73 }
74
75 // Explicit per-repo grants still work alongside membership: eve gets
76 // read on the private org repo.
77 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "krz/lib", "eve", "read"); code != 0 {
78 t.Fatalf("grant: %s", errOut)
79 }
80 mustGit(t, t.TempDir(), inst.gitEnv(eveKey), "clone", inst.sshURL("krz/lib"))
81
82 // Org repos list for members; org shows in org list.
83 out, _, _ := inst.ssh(t, bobKey, "", "repo", "list")
84 if !strings.Contains(out, "krz/lib") {
85 t.Fatalf("member repo list missing org repo:\n%s", out)
86 }
87 out, _, _ = inst.ssh(t, bobKey, "", "org", "list")
88 if !strings.Contains(out, "krz\tmember") {
89 t.Fatalf("org list: %s", out)
90 }
91
92 // Promotion works; the last admin is protected.
93 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "members", "add", "krz", "bob", "--role", "admin"); code != 0 {
94 t.Fatalf("promote: %s", errOut)
95 }
96 if _, errOut, code := inst.ssh(t, bobKey, "", "org", "members", "remove", "krz", "alice"); code != 0 {
97 t.Fatalf("bob (now admin) removing alice: %s", errOut)
98 }
99 _, errOut, code := inst.ssh(t, bobKey, "", "org", "members", "remove", "krz", "bob")
100 if code != 2 || !strings.Contains(errOut, "at least one admin") {
101 t.Fatalf("last admin removal: %d %s", code, errOut)
102 }
103
104 // Org deletion refuses while repos exist, then succeeds.
105 _, errOut, code = inst.ssh(t, bobKey, "", "org", "delete", "krz", "--yes")
106 if code != 1 || !strings.Contains(errOut, "still owns") {
107 t.Fatalf("delete with repos: %d %s", code, errOut)
108 }
109 if _, errOut, code = inst.ssh(t, bobKey, "", "repo", "delete", "krz/lib", "--yes"); code != 0 {
110 t.Fatalf("org repo delete: %s", errOut)
111 }
112 if _, errOut, code = inst.ssh(t, bobKey, "", "org", "delete", "krz", "--yes"); code != 0 {
113 t.Fatalf("org delete: %s", errOut)
114 }
115
116 // Transfer: org repo moves to a user; old path gone, new path clones,
117 // target collisions and non-admin transfers are refused.
118 if _, _, code = inst.ssh(t, aliceKey, "", "org", "create", "movers"); code != 0 {
119 t.Fatal("org movers failed")
120 }
121 if _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "movers/box"); code != 0 {
122 t.Fatal("movers/box failed")
123 }
124 tw := t.TempDir()
125 mustGit(t, tw, inst.gitEnv(aliceKey), "clone", inst.sshURL("movers/box"), "b1")
126 if _, errOut, code = inst.ssh(t, bobKey, "", "repo", "transfer", "movers/box", "bob"); code != 4 {
127 t.Fatalf("non-admin transfer: %d %s", code, errOut)
128 }
129 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "transfer", "movers/box", "alice"); code != 0 {
130 t.Fatalf("transfer: %s", errOut)
131 }
132 mustGit(t, tw, inst.gitEnv(aliceKey), "clone", inst.sshURL("alice/box"), "b2")
133 if out, code := gitRun(t, t.TempDir(), inst.gitEnv(aliceKey), "clone", inst.sshURL("movers/box")); code == 0 {
134 t.Fatalf("old transfer path still clones:\n%s", out)
135 }
136 if _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "movers/box"); code != 0 {
137 t.Fatal("recreate movers/box failed")
138 }
139 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "transfer", "movers/box", "alice"); code == 0 || !strings.Contains(errOut, "already") {
140 t.Fatalf("collision transfer: %d %s", code, errOut)
141 }
142
143 // Rename: clone works at the new path, old path is gone, collisions
144 // with users and existing orgs are refused.
145 if _, _, code = inst.ssh(t, aliceKey, "", "org", "create", "oldname"); code != 0 {
146 t.Fatal("org create oldname failed")
147 }
148 if _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "oldname/thing"); code != 0 {
149 t.Fatal("repo under oldname failed")
150 }
151 rnWork := t.TempDir()
152 mustGit(t, rnWork, inst.gitEnv(aliceKey), "clone", inst.sshURL("oldname/thing"), "w1")
153 if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "bob"); code != 2 || !strings.Contains(errOut, "taken") {
154 t.Fatalf("rename onto user name: %d %s", code, errOut)
155 }
156 if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "newname"); code != 0 {
157 t.Fatalf("rename: %s", errOut)
158 }
159 mustGit(t, rnWork, inst.gitEnv(aliceKey), "clone", inst.sshURL("newname/thing"), "w2")
160 if out, code := gitRun(t, t.TempDir(), inst.gitEnv(aliceKey), "clone", inst.sshURL("oldname/thing")); code == 0 {
161 t.Fatalf("old org path still clones:\n%s", out)
162 }
163 if out, _, _ := inst.ssh(t, aliceKey, "", "repo", "list"); !strings.Contains(out, "newname/thing") {
164 t.Fatalf("renamed org missing from repo list:\n%s", out)
165 }
166
167 // Public org repos appear on the anonymous web index.
168 if _, _, code = inst.ssh(t, aliceKey, "", "org", "create", "puborg"); code != 0 {
169 t.Fatal("org create failed")
170 }
171 if _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "puborg/site"); code != 0 {
172 t.Fatal("org public repo failed")
173 }
174 status, body := inst.get(t, "/")
175 if status != 200 || !strings.Contains(body, "puborg/site") {
176 t.Fatalf("org repo missing from index: %d", status)
177 }
178 if status, _ := inst.get(t, "/puborg/site"); status != 200 {
179 t.Fatalf("org repo page: %d", status)
180 }
181 status, body = inst.get(t, "/puborg")
182 if status != 200 || !strings.Contains(body, "org") || !strings.Contains(body, "alice") || !strings.Contains(body, ">site<") {
183 t.Fatalf("org owner page: %d\n%s", status, body)
184 }
185}