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