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