internal/control/orgscope_test.go

bd49b87fce895e9f0a7588152548fb6e1821ac7d
gitbay/internal/control/orgscope_test.go history · blame · raw

227 lines · 8310 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"strings"
  6	"testing"
  7
  8	"gitbay.org/gitbay/internal/config"
  9	"gitbay.org/gitbay/internal/protocol"
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13// orgFixture: alice admins org acme with acme/core (public) and acme/priv
 14// (private); bob is a plain member; carol is outside. alice also owns
 15// alice/app.
 16type orgFixture struct {
 17	st                *store.Store
 18	alice, bob, carol int64
 19	org               int64
 20	core, priv, app   store.Repo
 21}
 22
 23func newOrgFixture(t *testing.T) orgFixture {
 24	t.Helper()
 25	st, err := store.Open(":memory:")
 26	if err != nil {
 27		t.Fatal(err)
 28	}
 29	t.Cleanup(func() { st.Close() })
 30	if err := st.MigrateUp(); err != nil {
 31		t.Fatal(err)
 32	}
 33	var f orgFixture
 34	f.st = st
 35	user := func(name string) int64 {
 36		id, err := st.CreateUser(name, false)
 37		if err != nil {
 38			t.Fatal(err)
 39		}
 40		return id
 41	}
 42	f.alice, f.bob, f.carol = user("alice"), user("bob"), user("carol")
 43	if f.org, err = st.CreateOrg("acme", f.alice); err != nil {
 44		t.Fatal(err)
 45	}
 46	if err := st.SetOrgMember(f.org, f.bob, "member"); err != nil {
 47		t.Fatal(err)
 48	}
 49	repo := func(kind string, owner int64, name, vis string) store.Repo {
 50		id, err := st.CreateRepo(kind, owner, name, vis)
 51		if err != nil {
 52			t.Fatal(err)
 53		}
 54		r, _ := st.RepoByID(id)
 55		return r
 56	}
 57	f.core = repo("org", f.org, "core", "public")
 58	f.priv = repo("org", f.org, "priv", "private")
 59	f.app = repo("user", f.alice, "app", "public")
 60	return f
 61}
 62
 63func (f orgFixture) ctx(uid int64) (*Ctx, *bytes.Buffer) {
 64	var out bytes.Buffer
 65	name := map[int64]string{f.alice: "alice", f.bob: "bob", f.carol: "carol"}[uid]
 66	return &Ctx{
 67		User:   store.User{ID: uid, Username: name},
 68		Scope:  "full",
 69		Source: "SHA256:session",
 70		Store:  f.st,
 71		Cfg:    config.Config{Server: config.Server{SiteURL: "https://x.test"}},
 72		Stdin:  strings.NewReader(""),
 73		Stdout: &out,
 74		Stderr: &out,
 75		JSON:   true,
 76	}, &out
 77}
 78
 79func TestReadableOrgRepoIDs(t *testing.T) {
 80	f := newOrgFixture(t)
 81	ids, err := ReadableOrgRepoIDs(f.st, store.User{ID: f.bob, Username: "bob"}, f.org)
 82	if err != nil || len(ids) != 2 {
 83		t.Fatalf("member reads %v, %v; want both", ids, err)
 84	}
 85	ids, _ = ReadableOrgRepoIDs(f.st, store.User{ID: f.carol, Username: "carol"}, f.org)
 86	if len(ids) != 1 || ids[0] != f.core.ID {
 87		t.Fatalf("outsider reads %v; want core only", ids)
 88	}
 89	ids, _ = ReadableOrgRepoIDs(f.st, store.User{}, f.org)
 90	if len(ids) != 1 || ids[0] != f.core.ID {
 91		t.Fatalf("anonymous reads %v; want core only", ids)
 92	}
 93	ids, _ = ReadableScope(f.st, store.User{ID: f.alice, Username: "alice"}, f.app)
 94	if len(ids) != 1 || ids[0] != f.app.ID {
 95		t.Fatalf("user repo scope %v; want itself", ids)
 96	}
 97}
 98
 99func TestRepoLabelCommandsRefuseOrgNames(t *testing.T) {
100	f := newOrgFixture(t)
101	if _, err := f.st.SetOrgLabel(f.org, "bug", ""); err != nil {
102		t.Fatal(err)
103	}
104	c, out := f.ctx(f.alice)
105	if code := runLabelSet(c, []string{"acme/core", "bug", "--color", "ff0000"}); code != protocol.ExitFailure ||
106		!strings.Contains(out.String(), "org label set acme bug") {
107		t.Fatalf("label set over org name: exit %d %s", code, out.String())
108	}
109	out.Reset()
110	if code := runLabelRemove(c, []string{"acme/core", "bug"}); code != protocol.ExitFailure ||
111		!strings.Contains(out.String(), "org label remove acme bug") {
112		t.Fatalf("label remove of org row: exit %d %s", code, out.String())
113	}
114	out.Reset()
115	// issue label --add resolves to the org row, and label list marks it.
116	iid, _ := f.st.CreateIssue(f.core.ID, f.alice, "c1", "", "md")
117	_ = iid
118	if code := runIssueLabel(c, []string{"acme/core", "1", "--add", "bug"}); code != protocol.ExitOK {
119		t.Fatalf("issue label: exit %d %s", code, out.String())
120	}
121	out.Reset()
122	if code := runLabelList(c, []string{"acme/core"}); code != protocol.ExitOK ||
123		!strings.Contains(out.String(), `"org":true`) || !strings.Contains(out.String(), `"issues":1`) {
124		t.Fatalf("label list: exit %d %s", code, out.String())
125	}
126}
127
128func TestRepoMilestoneCommandsRefuseOrgTitles(t *testing.T) {
129	f := newOrgFixture(t)
130	if _, _, err := f.st.CreateOrgMilestone(f.org, "v1", "", ""); err != nil {
131		t.Fatal(err)
132	}
133	c, out := f.ctx(f.alice)
134	if code := runMilestoneCreate(c, []string{"acme/core", "v1"}); code != protocol.ExitFailure ||
135		!strings.Contains(out.String(), "org milestone create acme v1") {
136		t.Fatalf("milestone create over org title: exit %d %s", code, out.String())
137	}
138	out.Reset()
139	if code := runMilestoneClose(c, []string{"acme/core", "v1"}); code != protocol.ExitFailure ||
140		!strings.Contains(out.String(), "org milestone close acme v1") {
141		t.Fatalf("milestone close of org row: exit %d %s", code, out.String())
142	}
143	out.Reset()
144	// Attaching by title from a repo resolves the org milestone.
145	f.st.CreateIssue(f.core.ID, f.alice, "c1", "", "md")
146	if code := runIssueMilestone(c, []string{"acme/core", "1", "v1"}); code != protocol.ExitOK {
147		t.Fatalf("issue milestone: exit %d %s", code, out.String())
148	}
149	out.Reset()
150	if code := runMilestoneList(c, []string{"acme/core"}); code != protocol.ExitOK ||
151		!strings.Contains(out.String(), `"org":true`) || !strings.Contains(out.String(), `"open":1`) {
152		t.Fatalf("milestone list: exit %d %s", code, out.String())
153	}
154}
155
156// A duplicate title fails; it is not a usage error, and the repo-level
157// and org-level commands answer with the same code.
158func TestDuplicateMilestoneTitleFails(t *testing.T) {
159	f := newOrgFixture(t)
160	c, out := f.ctx(f.alice)
161	if code := runMilestoneCreate(c, []string{"alice/app", "v1"}); code != protocol.ExitOK {
162		t.Fatalf("first create: exit %d %s", code, out.String())
163	}
164	out.Reset()
165	if code := runMilestoneCreate(c, []string{"alice/app", "v1"}); code != protocol.ExitFailure {
166		t.Fatalf("duplicate create: exit %d %s", code, out.String())
167	}
168	out.Reset()
169	if code := runOrgMilestoneCreate(c, []string{"acme", "v1"}); code != protocol.ExitOK {
170		t.Fatalf("first org create: exit %d %s", code, out.String())
171	}
172	out.Reset()
173	if code := runOrgMilestoneCreate(c, []string{"acme", "v1"}); code != protocol.ExitFailure {
174		t.Fatalf("duplicate org create: exit %d %s", code, out.String())
175	}
176}
177
178// mr label mirrors issue label: a name the org holds resolves to the org's
179// row, one neither scope has is created in the repository, removing what
180// is not there is not found, and someone with read only is denied (#231).
181func TestMRLabelCommandMirrorsIssueLabel(t *testing.T) {
182	f := newOrgFixture(t)
183	if _, err := f.st.SetOrgLabel(f.org, "bug", ""); err != nil {
184		t.Fatal(err)
185	}
186	if _, err := f.st.CreateMR(f.core.ID, f.alice, f.core.ID, "topic", "main", "c1", "", "deadbeef", "md", false); err != nil {
187		t.Fatal(err)
188	}
189	c, out := f.ctx(f.alice)
190	if code := runMRLabel(c, []string{"acme/core", "1", "--add", "bug", "--add", "docs"}); code != protocol.ExitOK ||
191		!strings.Contains(out.String(), `"labels":["bug","docs"]`) {
192		t.Fatalf("mr label --add: exit %d %s", code, out.String())
193	}
194	// bug is the org's one row; docs was created in the repository.
195	var n int
196	f.st.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug'").Scan(&n)
197	if n != 1 {
198		t.Fatalf("labels named bug: %d, want 1", n)
199	}
200	if l, err := f.st.LabelByName(f.core, "docs"); err != nil || l.Org {
201		t.Fatalf("docs = %+v, %v", l, err)
202	}
203	out.Reset()
204	if code := runMRList(c, []string{"acme/core", "--label", "bug"}); code != protocol.ExitOK ||
205		!strings.Contains(out.String(), `"number":1`) {
206		t.Fatalf("mr list --label bug: exit %d %s", code, out.String())
207	}
208	out.Reset()
209	if code := runMRList(c, []string{"acme/core", "--label", "nope"}); code != protocol.ExitOK ||
210		strings.Contains(out.String(), `"number":1`) {
211		t.Fatalf("mr list --label nope: exit %d %s", code, out.String())
212	}
213	out.Reset()
214	if code := runMRLabel(c, []string{"acme/core", "1", "--remove", "docs"}); code != protocol.ExitOK ||
215		!strings.Contains(out.String(), `"labels":["bug"]`) {
216		t.Fatalf("mr label --remove: exit %d %s", code, out.String())
217	}
218	out.Reset()
219	if code := runMRLabel(c, []string{"acme/core", "1", "--remove", "docs"}); code != protocol.ExitNotFound {
220		t.Fatalf("mr label --remove of an absent label: exit %d %s", code, out.String())
221	}
222	// carol reads acme/core and writes nothing.
223	rc, rout := f.ctx(f.carol)
224	if code := runMRLabel(rc, []string{"acme/core", "1", "--add", "bug"}); code != protocol.ExitDenied {
225		t.Fatalf("reader: exit %d %s", code, rout.String())
226	}
227}