e2e/orgweb_test.go
207 lines · 7804 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "net/http"
6 "net/url"
7 "strings"
8 "testing"
9)
10
11// TestOrgManagementWeb covers running an organization from the browser:
12// membership and teams, admin-gated, dispatched through the same commands
13// the CLI uses.
14func TestOrgManagementWeb(t *testing.T) {
15 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
16 aliceKey := inst.newKey(t, "alice")
17 bobKey := inst.newKey(t, "bob")
18 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
19 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
20 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "acme"); code != 0 {
21 t.Fatalf("org create: %s", errOut)
22 }
23 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "acme/widget"); code != 0 {
24 t.Fatalf("repo create: %s", errOut)
25 }
26
27 alice := loginBrowser(t, inst, aliceKey)
28
29 // The management sections are admin-only: bob is not even a member.
30 bob := loginBrowser(t, inst, bobKey)
31 if _, body := browserGet(t, bob, inst.base()+"/acme"); strings.Contains(body, `value="member-add"`) {
32 t.Fatal("a non-member sees organization controls")
33 }
34 // And POSTing anyway is refused by the command, not by the template.
35 browserPost(t, bob, inst.base()+"/acme", url.Values{
36 "field": {"member-add"}, "user": {"bob"}, "role": {"admin"},
37 })
38 if members := orgMembers(t, inst, aliceKey); len(members) != 1 {
39 t.Fatalf("non-admin added themselves: %v", members)
40 }
41
42 status, body := browserGet(t, alice, inst.base()+"/acme")
43 if status != 200 || !strings.Contains(body, `value="member-add"`) {
44 t.Fatalf("admin sees no controls: %d", status)
45 }
46
47 // Add bob as a member through the form; confirm over SSH.
48 browserPost(t, alice, inst.base()+"/acme", url.Values{
49 "field": {"member-add"}, "user": {"bob"}, "role": {"member"},
50 })
51 if members := orgMembers(t, inst, aliceKey); len(members) != 2 {
52 t.Fatalf("member not added: %v", members)
53 }
54
55 // Create a team, put bob in it, and grant it write on the repo.
56 browserPost(t, alice, inst.base()+"/acme", url.Values{
57 "field": {"team-create"}, "team": {"builders"},
58 })
59 browserPost(t, alice, inst.base()+"/acme", url.Values{
60 "field": {"team-add"}, "team": {"builders"}, "user": {"bob"},
61 })
62 browserPost(t, alice, inst.base()+"/acme", url.Values{
63 "field": {"team-grant"}, "team": {"builders"},
64 "repo": {"acme/widget"}, "role": {"write"},
65 })
66 out, _, _ := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json")
67 if !strings.Contains(out, `"bob"`) || !strings.Contains(out, `"acme/widget"`) ||
68 !strings.Contains(out, `"write"`) {
69 t.Fatalf("team not configured: %s", out)
70 }
71 // The grant is real access, not just a row: bob can now push.
72 if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "show", "acme/widget"); code != 0 {
73 t.Fatalf("team grant did not confer access: %s", errOut)
74 }
75
76 // The page shows what was built.
77 _, body = browserGet(t, alice, inst.base()+"/acme")
78 for _, want := range []string{"builders", "acme/widget", "1 member"} {
79 if !strings.Contains(body, want) {
80 t.Errorf("org page missing %q", want)
81 }
82 }
83
84 // Revoking and removing work the same way round.
85 browserPost(t, alice, inst.base()+"/acme", url.Values{
86 "field": {"team-revoke"}, "team": {"builders"}, "repo": {"acme/widget"},
87 })
88
89 // Deleting the team needs its name typed; a bare post is refused and
90 // the team stays.
91 _, body = browserPost(t, alice, inst.base()+"/acme", url.Values{
92 "field": {"team-delete"}, "team": {"builders"},
93 })
94 if !strings.Contains(body, "type builders to confirm") {
95 t.Fatalf("unconfirmed team delete was not refused:\n%s", body)
96 }
97 if _, _, code := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); code != 0 {
98 t.Fatal("team deleted without confirmation")
99 }
100 browserPost(t, alice, inst.base()+"/acme", url.Values{
101 "field": {"team-delete"}, "team": {"builders"}, "confirm": {"builders"},
102 })
103 if _, _, code := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); code != 3 {
104 t.Fatalf("team not deleted: exit %d", code)
105 }
106
107 browserPost(t, alice, inst.base()+"/acme", url.Values{
108 "field": {"member-remove"}, "user": {"bob"},
109 })
110 if members := orgMembers(t, inst, aliceKey); len(members) != 1 {
111 t.Fatalf("member not removed: %v", members)
112 }
113}
114
115// loginBrowser mints a session over SSH and returns a browser holding it.
116func loginBrowser(t *testing.T, inst *instance, key string) *http.Client {
117 t.Helper()
118 out, errOut, code := inst.ssh(t, key, "", "web", "login", "--json")
119 if code != 0 {
120 t.Fatalf("web login: %s", errOut)
121 }
122 var env struct {
123 Data struct {
124 URL string `json:"url"`
125 } `json:"data"`
126 }
127 json.Unmarshal([]byte(out), &env)
128 c := newBrowser(t)
129 browserGet(t, c, inst.base()+env.Data.URL[strings.Index(env.Data.URL, "/login"):])
130 return c
131}
132
133func orgMembers(t *testing.T, inst *instance, key string) []string {
134 t.Helper()
135 out, _, _ := inst.ssh(t, key, "", "org", "members", "list", "acme", "--json")
136 var env struct {
137 Data struct {
138 Members []struct {
139 User string `json:"user"`
140 } `json:"members"`
141 } `json:"data"`
142 }
143 if err := json.Unmarshal([]byte(out), &env); err != nil {
144 t.Fatalf("members JSON: %v\n%s", err, out)
145 }
146 var names []string
147 for _, m := range env.Data.Members {
148 names = append(names, m.User)
149 }
150 return names
151}
152
153// The organization lifecycle from a browser: create from your own page,
154// rename from the org's. Delete stays on the CLI, where a typed
155// confirmation is the norm (#167).
156func TestOrgLifecycleWeb(t *testing.T) {
157 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
158 aliceKey := inst.newKey(t, "alice")
159 bobKey := inst.newKey(t, "bob")
160 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
161 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
162 alice := loginBrowser(t, inst, aliceKey)
163 bob := loginBrowser(t, inst, bobKey)
164
165 // The create form is on /new, beside the repository form, and no
166 // profile page carries it.
167 if _, body := browserGet(t, alice, inst.base()+"/new"); !strings.Contains(body, `value="org-create"`) {
168 t.Fatalf("no create form on /new:\n%s", body)
169 }
170 if _, body := browserGet(t, alice, inst.base()+"/alice"); strings.Contains(body, `value="org-create"`) {
171 t.Fatal("create form still on the profile page")
172 }
173
174 if status, _ := browserPost(t, alice, inst.base()+"/new", url.Values{
175 "field": {"org-create"}, "name": {"acmeco"}}); status != 200 {
176 t.Fatal("org create failed")
177 }
178 if out, _, _ := inst.ssh(t, aliceKey, "", "org", "list", "--json"); !strings.Contains(out, "acmeco") {
179 t.Fatalf("org not created:\n%s", out)
180 }
181
182 // Rename is offered to its admin, and the org moves.
183 _, body := browserGet(t, alice, inst.base()+"/acmeco")
184 if !strings.Contains(body, `value="org-rename"`) {
185 t.Fatalf("no rename form for the org admin:\n%s", body)
186 }
187 if !strings.Contains(body, "gitbay org delete") || strings.Contains(body, `value="org-delete"`) {
188 t.Error("delete is not recorded as a CLI operation")
189 }
190 if status, _ := browserPost(t, alice, inst.base()+"/acmeco", url.Values{
191 "field": {"org-rename"}, "name": {"acmeltd"}}); status != 200 {
192 t.Fatal("org rename failed")
193 }
194 if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 {
195 t.Fatal("renamed org not found under its new name")
196 }
197 if status, _ := browserGet(t, alice, inst.base()+"/acmeco"); status != http.StatusNotFound {
198 t.Errorf("old org name still resolves: %d", status)
199 }
200
201 // A non-admin cannot rename it, form or no form.
202 browserPost(t, bob, inst.base()+"/acmeltd", url.Values{
203 "field": {"org-rename"}, "name": {"bobsltd"}})
204 if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 {
205 t.Fatal("a non-admin renamed the organization")
206 }
207}