internal/deps/worker_test.go
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}