e2e/orgweb_test.go

69d8aa4ff9115d6ca1116e09a232e7a6416e4645
gitbay/e2e/orgweb_test.go history · blame · raw

206 lines · 7775 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 your own page and nobody else's.
166	if _, body := browserGet(t, alice, inst.base()+"/alice"); !strings.Contains(body, `value="org-create"`) {
167		t.Fatalf("no create form on your own page:\n%s", body)
168	}
169	if _, body := browserGet(t, bob, inst.base()+"/alice"); strings.Contains(body, `value="org-create"`) {
170		t.Fatal("create form on someone else's page")
171	}
172
173	if status, _ := browserPost(t, alice, inst.base()+"/alice", url.Values{
174		"field": {"org-create"}, "name": {"acmeco"}}); status != 200 {
175		t.Fatal("org create failed")
176	}
177	if out, _, _ := inst.ssh(t, aliceKey, "", "org", "list", "--json"); !strings.Contains(out, "acmeco") {
178		t.Fatalf("org not created:\n%s", out)
179	}
180
181	// Rename is offered to its admin, and the org moves.
182	_, body := browserGet(t, alice, inst.base()+"/acmeco")
183	if !strings.Contains(body, `value="org-rename"`) {
184		t.Fatalf("no rename form for the org admin:\n%s", body)
185	}
186	if !strings.Contains(body, "gitbay org delete") || strings.Contains(body, `value="org-delete"`) {
187		t.Error("delete is not recorded as a CLI operation")
188	}
189	if status, _ := browserPost(t, alice, inst.base()+"/acmeco", url.Values{
190		"field": {"org-rename"}, "name": {"acmeltd"}}); status != 200 {
191		t.Fatal("org rename failed")
192	}
193	if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 {
194		t.Fatal("renamed org not found under its new name")
195	}
196	if status, _ := browserGet(t, alice, inst.base()+"/acmeco"); status != http.StatusNotFound {
197		t.Errorf("old org name still resolves: %d", status)
198	}
199
200	// A non-admin cannot rename it, form or no form.
201	browserPost(t, bob, inst.base()+"/acmeltd", url.Values{
202		"field": {"org-rename"}, "name": {"bobsltd"}})
203	if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 {
204		t.Fatal("a non-admin renamed the organization")
205	}
206}