e2e/orgweb_test.go

419f6dfdc5489a0c6374e36dd1ebbfca68040056
gitbay/e2e/orgweb_test.go history · blame · raw

212 lines · 8052 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 work the same way round.
 89	browserPost(t, alice, inst.base()+"/acme", url.Values{
 90		"field": {"team-revoke"}, "team": {"builders"}, "repo": {"acme/widget"},
 91	})
 92
 93	// Deleting the team needs its name typed; a bare post is refused and
 94	// the team stays.
 95	_, body = browserPost(t, alice, inst.base()+"/acme", url.Values{
 96		"field": {"team-delete"}, "team": {"builders"},
 97	})
 98	if !strings.Contains(body, "type builders to confirm") {
 99		t.Fatalf("unconfirmed team delete was not refused:\n%s", body)
100	}
101	if _, _, code := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); code != 0 {
102		t.Fatal("team deleted without confirmation")
103	}
104	browserPost(t, alice, inst.base()+"/acme", url.Values{
105		"field": {"team-delete"}, "team": {"builders"}, "confirm": {"builders"},
106	})
107	if _, _, code := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); code != 3 {
108		t.Fatalf("team not deleted: exit %d", code)
109	}
110
111	browserPost(t, alice, inst.base()+"/acme", url.Values{
112		"field": {"member-remove"}, "user": {"bob"},
113	})
114	if members := orgMembers(t, inst, aliceKey); len(members) != 1 {
115		t.Fatalf("member not removed: %v", members)
116	}
117}
118
119// loginBrowser mints a session over SSH and returns a browser holding it.
120func loginBrowser(t *testing.T, inst *instance, key string) *http.Client {
121	t.Helper()
122	out, errOut, code := inst.ssh(t, key, "", "web", "login", "--json")
123	if code != 0 {
124		t.Fatalf("web login: %s", errOut)
125	}
126	var env struct {
127		Data struct {
128			URL string `json:"url"`
129		} `json:"data"`
130	}
131	json.Unmarshal([]byte(out), &env)
132	c := newBrowser(t)
133	browserGet(t, c, inst.base()+env.Data.URL[strings.Index(env.Data.URL, "/login"):])
134	return c
135}
136
137func orgMembers(t *testing.T, inst *instance, key string) []string {
138	t.Helper()
139	out, _, _ := inst.ssh(t, key, "", "org", "members", "list", "acme", "--json")
140	var env struct {
141		Data struct {
142			Members []struct {
143				User string `json:"user"`
144			} `json:"members"`
145		} `json:"data"`
146	}
147	if err := json.Unmarshal([]byte(out), &env); err != nil {
148		t.Fatalf("members JSON: %v\n%s", err, out)
149	}
150	var names []string
151	for _, m := range env.Data.Members {
152		names = append(names, m.User)
153	}
154	return names
155}
156
157// The organization lifecycle from a browser: create from your own page,
158// rename from the org's. Delete stays on the CLI, where a typed
159// confirmation is the norm (#167).
160func TestOrgLifecycleWeb(t *testing.T) {
161	t.Parallel()
162	inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
163	aliceKey := inst.newKey(t, "alice")
164	bobKey := inst.newKey(t, "bob")
165	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
166	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
167	alice := loginBrowser(t, inst, aliceKey)
168	bob := loginBrowser(t, inst, bobKey)
169
170	// The create form is on /new, beside the repository form, and no
171	// profile page carries it.
172	if _, body := browserGet(t, alice, inst.base()+"/new"); !strings.Contains(body, `value="org-create"`) {
173		t.Fatalf("no create form on /new:\n%s", body)
174	}
175	if _, body := browserGet(t, alice, inst.base()+"/alice"); strings.Contains(body, `value="org-create"`) {
176		t.Fatal("create form still on the profile page")
177	}
178
179	if status, _ := browserPost(t, alice, inst.base()+"/new", url.Values{
180		"field": {"org-create"}, "name": {"acmeco"}}); status != 200 {
181		t.Fatal("org create failed")
182	}
183	if out, _, _ := inst.ssh(t, aliceKey, "", "org", "list", "--json"); !strings.Contains(out, "acmeco") {
184		t.Fatalf("org not created:\n%s", out)
185	}
186
187	// Rename is offered to its admin, and the org moves.
188	_, body := browserGet(t, alice, inst.base()+"/acmeco/-/people")
189	if !strings.Contains(body, `value="org-rename"`) {
190		t.Fatalf("no rename form for the org admin:\n%s", body)
191	}
192	if !strings.Contains(body, "gitbay org delete") || strings.Contains(body, `value="org-delete"`) {
193		t.Error("delete is not recorded as a CLI operation")
194	}
195	if status, _ := browserPost(t, alice, inst.base()+"/acmeco", url.Values{
196		"field": {"org-rename"}, "name": {"acmeltd"}}); status != 200 {
197		t.Fatal("org rename failed")
198	}
199	if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 {
200		t.Fatal("renamed org not found under its new name")
201	}
202	if status, _ := browserGet(t, alice, inst.base()+"/acmeco"); status != http.StatusNotFound {
203		t.Errorf("old org name still resolves: %d", status)
204	}
205
206	// A non-admin cannot rename it, form or no form.
207	browserPost(t, bob, inst.base()+"/acmeltd", url.Values{
208		"field": {"org-rename"}, "name": {"bobsltd"}})
209	if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 {
210		t.Fatal("a non-admin renamed the organization")
211	}
212}