e2e/orgweb_test.go

ba0a7d33f3a65ce53aafb074fda1682cf1cecfdf
gitbay/e2e/orgweb_test.go history · blame · raw

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