krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

repo-descriptions: e2e/org_test.go · raw

  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}