internal/deps/worker_test.go

784b5dfad3f6ed718ada2a43910225c220abb310
gitbay/internal/deps/worker_test.go history · blame · raw

225 lines · 7020 bytes

  1package deps
  2
  3import (
  4	"context"
  5	"path/filepath"
  6	"strings"
  7	"testing"
  8
  9	"gitbay.org/gitbay/internal/config"
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13func testWorker(t *testing.T) (*Worker, store.Repo) {
 14	t.Helper()
 15	st, err := store.Open(filepath.Join(t.TempDir(), "gitbay.db"))
 16	if err != nil {
 17		t.Fatal(err)
 18	}
 19	t.Cleanup(func() { st.Close() })
 20	if err := st.MigrateUp(); err != nil {
 21		t.Fatal(err)
 22	}
 23	owner, err := st.CreateUser("alice", false)
 24	if err != nil {
 25		t.Fatal(err)
 26	}
 27	if err := st.AddEmail(owner, "alice@example.com", "admin", true); err != nil {
 28		t.Fatal(err)
 29	}
 30	id, err := st.CreateRepo("user", owner, "thing", "public")
 31	if err != nil {
 32		t.Fatal(err)
 33	}
 34	repo, err := st.RepoByID(id)
 35	if err != nil {
 36		t.Fatal(err)
 37	}
 38	if err := st.EnableDepCheck(repo.ID); err != nil {
 39		t.Fatal(err)
 40	}
 41	cfg := config.Default()
 42	cfg.Server.SiteURL = "https://gitbay.test"
 43	cfg.Mail.SMTPHost = "localhost:587"
 44	return &Worker{St: st, Cfg: cfg}, repo
 45}
 46
 47func reports(pairs ...string) []store.DepReport {
 48	var out []store.DepReport
 49	for i := 0; i < len(pairs); i += 3 {
 50		out = append(out, store.DepReport{
 51			Ecosystem: EcoGo, Name: pairs[i], Current: pairs[i+1], Latest: pairs[i+2]})
 52	}
 53	return out
 54}
 55
 56func TestReconcileIssueLifecycle(t *testing.T) {
 57	w, repo := testWorker(t)
 58
 59	// Nothing behind: no issue, no mail.
 60	if err := w.reconcile(repo, nil); err != nil {
 61		t.Fatal(err)
 62	}
 63	if check, _ := w.St.DepCheckFor(repo.ID); check.IssueNumber != 0 {
 64		t.Fatalf("issue %d opened with nothing behind", check.IssueNumber)
 65	}
 66
 67	// Something falls behind: one issue, one notification.
 68	if err := w.reconcile(repo, reports("github.com/a/b", "v1.0.0", "v1.1.0")); err != nil {
 69		t.Fatal(err)
 70	}
 71	check, err := w.St.DepCheckFor(repo.ID)
 72	if err != nil || check.IssueNumber == 0 {
 73		t.Fatalf("no issue opened: %v", err)
 74	}
 75	issue, err := w.St.IssueByNumber(repo.ID, check.IssueNumber)
 76	if err != nil {
 77		t.Fatal(err)
 78	}
 79	if issue.Author != store.BotUsername {
 80		t.Errorf("issue author = %q, want %q", issue.Author, store.BotUsername)
 81	}
 82	if issue.Title != IssueTitle {
 83		t.Errorf("issue title = %q", issue.Title)
 84	}
 85	if !strings.Contains(issue.Body, "github.com/a/b") || !strings.Contains(issue.Body, "v1.1.0") {
 86		t.Errorf("issue body missing the dependency:\n%s", issue.Body)
 87	}
 88	mail, err := w.St.DueMail(10)
 89	if err != nil {
 90		t.Fatal(err)
 91	}
 92	if len(mail) != 1 || mail[0].Recipient != "alice@example.com" {
 93		t.Fatalf("mail = %+v, want one to the owner", mail)
 94	}
 95
 96	// Same set again: the issue is left alone and nobody is mailed twice.
 97	if err := w.reconcile(repo, reports("github.com/a/b", "v1.0.0", "v1.1.0")); err != nil {
 98		t.Fatal(err)
 99	}
100	if mail, _ := w.St.DueMail(10); len(mail) != 1 {
101		t.Errorf("unchanged set mailed again: %d messages", len(mail))
102	}
103
104	// The set changes: same issue, rewritten body, another notification.
105	if err := w.reconcile(repo, reports("github.com/a/b", "v1.0.0", "v1.2.0")); err != nil {
106		t.Fatal(err)
107	}
108	after, _ := w.St.DepCheckFor(repo.ID)
109	if after.IssueNumber != check.IssueNumber {
110		t.Errorf("second issue opened: %d then %d", check.IssueNumber, after.IssueNumber)
111	}
112	issue, _ = w.St.IssueByNumber(repo.ID, check.IssueNumber)
113	if !strings.Contains(issue.Body, "v1.2.0") {
114		t.Errorf("issue body not rewritten:\n%s", issue.Body)
115	}
116	if mail, _ := w.St.DueMail(10); len(mail) != 2 {
117		t.Errorf("changed set produced %d messages, want 2", len(mail))
118	}
119
120	// Caught up: the issue closes and the reports are forgotten.
121	if err := w.reconcile(repo, nil); err != nil {
122		t.Fatal(err)
123	}
124	issue, _ = w.St.IssueByNumber(repo.ID, check.IssueNumber)
125	if issue.State != "closed" {
126		t.Errorf("issue state = %q, want closed", issue.State)
127	}
128	if left, _ := w.St.ReportedDeps(repo.ID); len(left) != 0 {
129		t.Errorf("reports left behind: %v", left)
130	}
131	if mail, _ := w.St.DueMail(10); len(mail) != 2 {
132		t.Errorf("closing mailed: %d messages", len(mail))
133	}
134}
135
136func TestReconcileOpensFreshIssueAfterClose(t *testing.T) {
137	w, repo := testWorker(t)
138	if err := w.reconcile(repo, reports("github.com/a/b", "v1.0.0", "v1.1.0")); err != nil {
139		t.Fatal(err)
140	}
141	first, _ := w.St.DepCheckFor(repo.ID)
142	issue, _ := w.St.IssueByNumber(repo.ID, first.IssueNumber)
143
144	// The maintainer closes it. The worker does not reopen it; the next
145	// change gets its own issue.
146	if err := w.St.SetIssueState(issue.ID, "closed"); err != nil {
147		t.Fatal(err)
148	}
149	if err := w.reconcile(repo, reports("github.com/a/b", "v1.0.0", "v1.3.0")); err != nil {
150		t.Fatal(err)
151	}
152	second, _ := w.St.DepCheckFor(repo.ID)
153	if second.IssueNumber == first.IssueNumber {
154		t.Fatalf("reused closed issue #%d", first.IssueNumber)
155	}
156	if reopened, _ := w.St.IssueByNumber(repo.ID, first.IssueNumber); reopened.State != "closed" {
157		t.Error("the closed issue was reopened")
158	}
159}
160
161func TestBehindQueriesRegistries(t *testing.T) {
162	srv, _ := fakeRegistry(t, map[string]string{
163		"/github.com/a/b/@latest": `{"Version":"v1.1.0"}`,
164		"/github.com/c/d/@latest": `{"Version":"v2.0.0"}`,
165	})
166	w, _ := testWorker(t)
167	w.Client = NewClient("test")
168	w.Client.Hosts = map[string]string{EcoGo: srv.URL}
169
170	got, err := w.behind(context.Background(), []Dep{
171		{Ecosystem: EcoGo, Name: "github.com/a/b", Current: "v1.0.0"}, // behind
172		{Ecosystem: EcoGo, Name: "github.com/c/d", Current: "v2.0.0"}, // current
173	})
174	if err != nil {
175		t.Fatal(err)
176	}
177	if len(got) != 1 || got[0].Name != "github.com/a/b" || got[0].Latest != "v1.1.0" {
178		t.Fatalf("behind = %+v", got)
179	}
180}
181
182func TestBehindToleratesOneFailureButNotAll(t *testing.T) {
183	srv, _ := fakeRegistry(t, map[string]string{"/github.com/a/b/@latest": `{"Version":"v1.1.0"}`})
184	w, _ := testWorker(t)
185	w.Client = NewClient("test")
186	w.Client.Hosts = map[string]string{EcoGo: srv.URL}
187	found := []Dep{
188		{Ecosystem: EcoGo, Name: "github.com/a/b", Current: "v1.0.0"},
189		{Ecosystem: EcoGo, Name: "github.com/gone/away", Current: "v1.0.0"}, // 404s
190	}
191	got, err := w.behind(context.Background(), found)
192	if err != nil {
193		t.Fatalf("one failed lookup failed the sweep: %v", err)
194	}
195	if len(got) != 1 {
196		t.Fatalf("behind = %+v", got)
197	}
198	if _, err := w.behind(context.Background(), found[1:]); err == nil {
199		t.Error("every lookup failing was reported as success")
200	}
201}
202
203func TestReconcileLeavesClosedIssueClosedOnUnchangedSet(t *testing.T) {
204	w, repo := testWorker(t)
205	behind := reports("github.com/a/b", "v1.0.0", "v1.1.0")
206	if err := w.reconcile(repo, behind); err != nil {
207		t.Fatal(err)
208	}
209	check, _ := w.St.DepCheckFor(repo.ID)
210	issue, _ := w.St.IssueByNumber(repo.ID, check.IssueNumber)
211	if err := w.St.SetIssueState(issue.ID, "closed"); err != nil {
212		t.Fatal(err)
213	}
214	// Nothing has changed, so closing the issue has to stick.
215	if err := w.reconcile(repo, behind); err != nil {
216		t.Fatal(err)
217	}
218	after, _ := w.St.DepCheckFor(repo.ID)
219	if after.IssueNumber != check.IssueNumber {
220		t.Fatalf("opened issue #%d on an unchanged set", after.IssueNumber)
221	}
222	if again, _ := w.St.IssueByNumber(repo.ID, check.IssueNumber); again.State != "closed" {
223		t.Error("the closed issue came back")
224	}
225}