1package github
2
3import (
4 "context"
5 "net/url"
6 "testing"
7 "time"
8
9 "github.com/pkg/errors"
10 "github.com/shurcooL/githubv4"
11 m "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13
14 "github.com/MichaelMure/git-bug/bridge/github/mocks"
15 "github.com/MichaelMure/git-bug/cache"
16 "github.com/MichaelMure/git-bug/entities/bug"
17 "github.com/MichaelMure/git-bug/repository"
18 "github.com/MichaelMure/git-bug/util/interrupt"
19)
20
21// using testify/mock and mockery
22
23var userName = githubv4.String("marcus")
24var userEmail = githubv4.String("marcus@rom.com")
25var unedited = githubv4.String("unedited")
26var edited = githubv4.String("edited")
27
28func TestGithubImporterIntegration(t *testing.T) {
29 // mock
30 clientMock := &mocks.Client{}
31 setupExpectations(t, clientMock)
32 importer := githubImporter{}
33 importer.client = &rateLimitHandlerClient{sc: clientMock}
34
35 // arrange
36 repo := repository.CreateGoGitTestRepo(t, false)
37 backend, buildEvents, err := cache.NewRepoCache(repo)
38 require.NoError(t, err)
39 for event := range buildEvents {
40 require.NoError(t, event.Err)
41 }
42 defer backend.Close()
43 interrupt.RegisterCleaner(backend.Close)
44 require.NoError(t, err)
45
46 // act
47 events, err := importer.ImportAll(context.Background(), backend, time.Time{})
48
49 // assert
50 require.NoError(t, err)
51 for e := range events {
52 require.NoError(t, e.Err)
53 }
54 require.Len(t, backend.Bugs().AllIds(), 5)
55 require.Len(t, backend.Identities().AllIds(), 2)
56
57 b1, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/1")
58 require.NoError(t, err)
59 ops1 := b1.Snapshot().Operations
60 require.Equal(t, "marcus", ops1[0].Author().Name())
61 require.Equal(t, "title 1", ops1[0].(*bug.CreateOperation).Title)
62 require.Equal(t, "body text 1", ops1[0].(*bug.CreateOperation).Message)
63
64 b3, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/3")
65 require.NoError(t, err)
66 ops3 := b3.Snapshot().Operations
67 require.Equal(t, "issue 3 comment 1", ops3[1].(*bug.AddCommentOperation).Message)
68 require.Equal(t, "issue 3 comment 2", ops3[2].(*bug.AddCommentOperation).Message)
69 require.Equal(t, []bug.Label{"bug"}, ops3[3].(*bug.LabelChangeOperation).Added)
70 require.Equal(t, "title 3, edit 1", ops3[4].(*bug.SetTitleOperation).Title)
71
72 b4, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/4")
73 require.NoError(t, err)
74 ops4 := b4.Snapshot().Operations
75 require.Equal(t, "edited", ops4[1].(*bug.EditCommentOperation).Message)
76
77}
78
79func setupExpectations(t *testing.T, mock *mocks.Client) {
80 rateLimitingError(mock)
81 expectIssueQuery1(mock)
82 expectIssueQuery2(mock)
83 expectIssueQuery3(mock)
84 expectUserQuery(t, mock)
85}
86
87func rateLimitingError(mock *mocks.Client) {
88 mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(errors.New("API rate limit exceeded")).Once()
89 mock.On("Query", m.Anything, m.AnythingOfType("*github.rateLimitQuery"), m.Anything).Return(nil).Run(
90 func(args m.Arguments) {
91 retVal := args.Get(1).(*rateLimitQuery)
92 retVal.RateLimit.ResetAt.Time = time.Now().Add(time.Millisecond * 200)
93 },
94 ).Once()
95}
96
97func expectIssueQuery1(mock *mocks.Client) {
98 mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(nil).Run(
99 func(args m.Arguments) {
100 retVal := args.Get(1).(*issueQuery)
101 retVal.Repository.Issues.Nodes = []issueNode{
102 {
103 issue: issue{
104 authorEvent: authorEvent{
105 Id: 1,
106 Author: &actor{
107 Typename: "User",
108 User: userActor{
109 Name: &userName,
110 Email: userEmail,
111 },
112 },
113 },
114 Title: "title 1",
115 Number: 1,
116 Body: "body text 1",
117 Url: githubv4.URI{
118 URL: &url.URL{
119 Scheme: "https",
120 Host: "github.com",
121 Path: "marcus/to-himself/issues/1",
122 },
123 },
124 },
125 UserContentEdits: userContentEditConnection{},
126 TimelineItems: timelineItemsConnection{},
127 },
128 {
129 issue: issue{
130 authorEvent: authorEvent{
131 Id: 2,
132 Author: &actor{
133 Typename: "User",
134 User: userActor{
135 Name: &userName,
136 Email: userEmail,
137 },
138 },
139 },
140 Title: "title 2",
141 Number: 2,
142 Body: "body text 2",
143 Url: githubv4.URI{
144 URL: &url.URL{
145 Scheme: "https",
146 Host: "github.com",
147 Path: "marcus/to-himself/issues/2",
148 },
149 },
150 },
151 UserContentEdits: userContentEditConnection{},
152 TimelineItems: timelineItemsConnection{},
153 },
154 }
155 retVal.Repository.Issues.PageInfo = pageInfo{
156 EndCursor: "end-cursor-1",
157 HasNextPage: true,
158 }
159 },
160 ).Once()
161}
162
163func expectIssueQuery2(mock *mocks.Client) {
164 mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(nil).Run(
165 func(args m.Arguments) {
166 retVal := args.Get(1).(*issueQuery)
167 retVal.Repository.Issues.Nodes = []issueNode{
168 {
169 issue: issue{
170 authorEvent: authorEvent{
171 Id: 3,
172 Author: &actor{
173 Typename: "User",
174 User: userActor{
175 Name: &userName,
176 Email: userEmail,
177 },
178 },
179 },
180 Title: "title 3",
181 Number: 3,
182 Body: "body text 3",
183 Url: githubv4.URI{
184 URL: &url.URL{
185 Scheme: "https",
186 Host: "github.com",
187 Path: "marcus/to-himself/issues/3",
188 },
189 },
190 },
191 UserContentEdits: userContentEditConnection{},
192 TimelineItems: timelineItemsConnection{
193 Nodes: []timelineItem{
194 {
195 Typename: "IssueComment",
196 IssueComment: issueComment{
197 authorEvent: authorEvent{
198 Id: 301,
199 Author: &actor{
200 Typename: "User",
201 User: userActor{
202 Name: &userName,
203 Email: userEmail,
204 },
205 },
206 },
207 Body: "issue 3 comment 1",
208 Url: githubv4.URI{
209 URL: &url.URL{
210 Scheme: "https",
211 Host: "github.com",
212 Path: "marcus/to-himself/issues/3#issuecomment-1",
213 },
214 },
215 UserContentEdits: userContentEditConnection{},
216 },
217 },
218 {
219 Typename: "IssueComment",
220 IssueComment: issueComment{
221 authorEvent: authorEvent{
222 Id: 302,
223 Author: &actor{
224 Typename: "User",
225 User: userActor{
226 Name: &userName,
227 Email: userEmail,
228 },
229 },
230 },
231 Body: "issue 3 comment 2",
232 Url: githubv4.URI{
233 URL: &url.URL{
234 Scheme: "https",
235 Host: "github.com",
236 Path: "marcus/to-himself/issues/3#issuecomment-2",
237 },
238 },
239 UserContentEdits: userContentEditConnection{},
240 },
241 },
242 {
243 Typename: "LabeledEvent",
244 LabeledEvent: labeledEvent{
245 actorEvent: actorEvent{
246 Id: 303,
247 Actor: &actor{
248 Typename: "User",
249 User: userActor{
250 Name: &userName,
251 Email: userEmail,
252 },
253 },
254 },
255 Label: label{
256 Name: "bug",
257 },
258 },
259 },
260 {
261 Typename: "RenamedTitleEvent",
262 RenamedTitleEvent: renamedTitleEvent{
263 actorEvent: actorEvent{
264 Id: 304,
265 Actor: &actor{
266 Typename: "User",
267 User: userActor{
268 Name: &userName,
269 Email: userEmail,
270 },
271 },
272 },
273 CurrentTitle: "title 3, edit 1",
274 },
275 },
276 },
277 PageInfo: pageInfo{},
278 },
279 },
280 {
281 issue: issue{
282 authorEvent: authorEvent{
283 Id: 4,
284 Author: &actor{
285 Typename: "User",
286 User: userActor{
287 Name: &userName,
288 Email: userEmail,
289 },
290 },
291 },
292 Title: "title 4",
293 Number: 4,
294 Body: unedited,
295 Url: githubv4.URI{
296 URL: &url.URL{
297 Scheme: "https",
298 Host: "github.com",
299 Path: "marcus/to-himself/issues/4",
300 },
301 },
302 },
303 UserContentEdits: userContentEditConnection{
304 Nodes: []userContentEdit{
305 // Github is weird: here the order is reversed chronological
306 {
307 Id: 402,
308 Editor: &actor{
309 Typename: "User",
310 User: userActor{
311 Name: &userName,
312 Email: userEmail,
313 },
314 },
315 Diff: &edited,
316 },
317 {
318 Id: 401,
319 Editor: &actor{
320 Typename: "User",
321 User: userActor{
322 Name: &userName,
323 Email: userEmail,
324 },
325 },
326 // Github is weird: whenever an issue has issue edits, then the first item
327 // (issue edit) holds the original (unedited) content and the second item
328 // (issue edit) holds the (first) edited content.
329 Diff: &unedited,
330 },
331 },
332 PageInfo: pageInfo{},
333 },
334 TimelineItems: timelineItemsConnection{},
335 },
336 }
337 retVal.Repository.Issues.PageInfo = pageInfo{
338 EndCursor: "end-cursor-2",
339 HasNextPage: true,
340 }
341 },
342 ).Once()
343}
344
345func expectIssueQuery3(mock *mocks.Client) {
346 mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(nil).Run(
347 func(args m.Arguments) {
348 retVal := args.Get(1).(*issueQuery)
349 retVal.Repository.Issues.Nodes = []issueNode{
350 {
351 issue: issue{
352 authorEvent: authorEvent{
353 Author: nil,
354 },
355 Title: "title 5",
356 Number: 5,
357 Body: "body text 5",
358 Url: githubv4.URI{
359 URL: &url.URL{
360 Scheme: "https",
361 Host: "github.com",
362 Path: "marcus/to-himself/issues/5",
363 },
364 },
365 },
366 UserContentEdits: userContentEditConnection{},
367 TimelineItems: timelineItemsConnection{},
368 },
369 }
370 retVal.Repository.Issues.PageInfo = pageInfo{}
371 },
372 ).Once()
373}
374
375func expectUserQuery(t *testing.T, mock *mocks.Client) {
376 mock.On("Query", m.Anything, m.AnythingOfType("*github.userQuery"), m.AnythingOfType("map[string]interface {}")).Return(nil).Run(
377 func(args m.Arguments) {
378 vars := args.Get(2).(map[string]interface{})
379 ghost := githubv4.String("ghost")
380 require.Equal(t, ghost, vars["login"])
381
382 retVal := args.Get(1).(*userQuery)
383 retVal.User.Name = &ghost
384 retVal.User.Login = "ghost-login"
385 },
386 ).Once()
387}