diff options
Diffstat (limited to 'bridge')
-rw-r--r-- | bridge/github/import_integration_test.go | 384 | ||||
-rw-r--r-- | bridge/github/import_query.go | 56 | ||||
-rw-r--r-- | bridge/github/mocks/Client.go | 44 |
3 files changed, 461 insertions, 23 deletions
diff --git a/bridge/github/import_integration_test.go b/bridge/github/import_integration_test.go new file mode 100644 index 00000000..3349f3f5 --- /dev/null +++ b/bridge/github/import_integration_test.go @@ -0,0 +1,384 @@ +package github + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/MichaelMure/git-bug/bridge/github/mocks" + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/interrupt" + "github.com/pkg/errors" + "github.com/shurcooL/githubv4" + m "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// using testify/mock and mockery + +var userName = githubv4.String("marcus") +var userEmail = githubv4.String("marcus@rom.com") +var unedited = githubv4.String("unedited") +var edited = githubv4.String("edited") + +func TestGithubImporterIntegration(t *testing.T) { + // mock + clientMock := &mocks.Client{} + setupExpectations(t, clientMock) + importer := githubImporter{} + importer.client = &rateLimitHandlerClient{sc: clientMock} + + // arrange + repo := repository.CreateGoGitTestRepo(false) + defer repository.CleanupTestRepos(repo) + backend, err := cache.NewRepoCache(repo) + require.NoError(t, err) + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) + require.NoError(t, err) + + // act + events, err := importer.ImportAll(context.Background(), backend, time.Time{}) + + // assert + require.NoError(t, err) + for e := range events { + require.NoError(t, e.Err) + } + require.Len(t, backend.AllBugsIds(), 5) + require.Len(t, backend.AllIdentityIds(), 2) + + b1, err := backend.ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/1") + require.NoError(t, err) + ops1 := b1.Snapshot().Operations + require.Equal(t, "marcus", ops1[0].Author().Name()) + require.Equal(t, "title 1", ops1[0].(*bug.CreateOperation).Title) + require.Equal(t, "body text 1", ops1[0].(*bug.CreateOperation).Message) + + b3, err := backend.ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/3") + require.NoError(t, err) + ops3 := b3.Snapshot().Operations + require.Equal(t, "issue 3 comment 1", ops3[1].(*bug.AddCommentOperation).Message) + require.Equal(t, "issue 3 comment 2", ops3[2].(*bug.AddCommentOperation).Message) + require.Equal(t, []bug.Label{"bug"}, ops3[3].(*bug.LabelChangeOperation).Added) + require.Equal(t, "title 3, edit 1", ops3[4].(*bug.SetTitleOperation).Title) + + b4, err := backend.ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/4") + require.NoError(t, err) + ops4 := b4.Snapshot().Operations + require.Equal(t, "edited", ops4[1].(*bug.EditCommentOperation).Message) + +} + +func setupExpectations(t *testing.T, mock *mocks.Client) { + rateLimitingError(mock) + expectIssueQuery1(mock) + expectIssueQuery2(mock) + expectIssueQuery3(mock) + expectUserQuery(t, mock) +} + +func rateLimitingError(mock *mocks.Client) { + mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(errors.New("API rate limit exceeded")).Once() + mock.On("Query", m.Anything, m.AnythingOfType("*github.rateLimitQuery"), m.Anything).Return(nil).Run( + func(args m.Arguments) { + retVal := args.Get(1).(*rateLimitQuery) + retVal.RateLimit.ResetAt.Time = time.Now().Add(time.Millisecond * 200) + }, + ).Once() +} + +func expectIssueQuery1(mock *mocks.Client) { + mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(nil).Run( + func(args m.Arguments) { + retVal := args.Get(1).(*issueQuery) + retVal.Repository.Issues.Nodes = []issueNode{ + { + issue: issue{ + authorEvent: authorEvent{ + Id: 1, + Author: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Title: "title 1", + Number: 1, + Body: "body text 1", + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/1", + }, + }, + }, + UserContentEdits: userContentEditConnection{}, + TimelineItems: timelineItemsConnection{}, + }, + { + issue: issue{ + authorEvent: authorEvent{ + Id: 2, + Author: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Title: "title 2", + Number: 2, + Body: "body text 2", + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/2", + }, + }, + }, + UserContentEdits: userContentEditConnection{}, + TimelineItems: timelineItemsConnection{}, + }, + } + retVal.Repository.Issues.PageInfo = pageInfo{ + EndCursor: "end-cursor-1", + HasNextPage: true, + } + }, + ).Once() +} + +func expectIssueQuery2(mock *mocks.Client) { + mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(nil).Run( + func(args m.Arguments) { + retVal := args.Get(1).(*issueQuery) + retVal.Repository.Issues.Nodes = []issueNode{ + { + issue: issue{ + authorEvent: authorEvent{ + Id: 3, + Author: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Title: "title 3", + Number: 3, + Body: "body text 3", + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/3", + }, + }, + }, + UserContentEdits: userContentEditConnection{}, + TimelineItems: timelineItemsConnection{ + Nodes: []timelineItem{ + { + Typename: "IssueComment", + IssueComment: issueComment{ + authorEvent: authorEvent{ + Id: 301, + Author: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Body: "issue 3 comment 1", + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/3#issuecomment-1", + }, + }, + UserContentEdits: userContentEditConnection{}, + }, + }, + { + Typename: "IssueComment", + IssueComment: issueComment{ + authorEvent: authorEvent{ + Id: 302, + Author: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Body: "issue 3 comment 2", + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/3#issuecomment-2", + }, + }, + UserContentEdits: userContentEditConnection{}, + }, + }, + { + Typename: "LabeledEvent", + LabeledEvent: labeledEvent{ + actorEvent: actorEvent{ + Id: 303, + Actor: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Label: label{ + Name: "bug", + }, + }, + }, + { + Typename: "RenamedTitleEvent", + RenamedTitleEvent: renamedTitleEvent{ + actorEvent: actorEvent{ + Id: 304, + Actor: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + CurrentTitle: "title 3, edit 1", + }, + }, + }, + PageInfo: pageInfo{}, + }, + }, + { + issue: issue{ + authorEvent: authorEvent{ + Id: 4, + Author: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + }, + Title: "title 4", + Number: 4, + Body: unedited, + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/4", + }, + }, + }, + UserContentEdits: userContentEditConnection{ + Nodes: []userContentEdit{ + // Github is weird: here the order is reversed chronological + { + Id: 402, + Editor: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + Diff: &edited, + }, + { + Id: 401, + Editor: &actor{ + Typename: "User", + User: userActor{ + Name: &userName, + Email: userEmail, + }, + }, + // Github is weird: whenever an issue has issue edits, then the first item + // (issue edit) holds the original (unedited) content and the second item + // (issue edit) holds the (first) edited content. + Diff: &unedited, + }, + }, + PageInfo: pageInfo{}, + }, + TimelineItems: timelineItemsConnection{}, + }, + } + retVal.Repository.Issues.PageInfo = pageInfo{ + EndCursor: "end-cursor-2", + HasNextPage: true, + } + }, + ).Once() +} + +func expectIssueQuery3(mock *mocks.Client) { + mock.On("Query", m.Anything, m.AnythingOfType("*github.issueQuery"), m.Anything).Return(nil).Run( + func(args m.Arguments) { + retVal := args.Get(1).(*issueQuery) + retVal.Repository.Issues.Nodes = []issueNode{ + { + issue: issue{ + authorEvent: authorEvent{ + Author: nil, + }, + Title: "title 5", + Number: 5, + Body: "body text 5", + Url: githubv4.URI{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "marcus/to-himself/issues/5", + }, + }, + }, + UserContentEdits: userContentEditConnection{}, + TimelineItems: timelineItemsConnection{}, + }, + } + retVal.Repository.Issues.PageInfo = pageInfo{} + }, + ).Once() +} + +func expectUserQuery(t *testing.T, mock *mocks.Client) { + mock.On("Query", m.Anything, m.AnythingOfType("*github.userQuery"), m.AnythingOfType("map[string]interface {}")).Return(nil).Run( + func(args m.Arguments) { + vars := args.Get(2).(map[string]interface{}) + ghost := githubv4.String("ghost") + require.Equal(t, ghost, vars["login"]) + + retVal := args.Get(1).(*userQuery) + retVal.User.Name = &ghost + retVal.User.Login = "ghost-login" + }, + ).Once() +} diff --git a/bridge/github/import_query.go b/bridge/github/import_query.go index 1428e3fd..bbe0ec96 100644 --- a/bridge/github/import_query.go +++ b/bridge/github/import_query.go @@ -113,6 +113,26 @@ type userContentEdit struct { Diff *githubv4.String } +type label struct { + Name githubv4.String +} + +type labeledEvent struct { + actorEvent + Label label +} + +type unlabeledEvent struct { + actorEvent + Label label +} + +type renamedTitleEvent struct { + actorEvent + CurrentTitle githubv4.String + PreviousTitle githubv4.String +} + type timelineItem struct { Typename githubv4.String `graphql:"__typename"` @@ -120,20 +140,8 @@ type timelineItem struct { IssueComment issueComment `graphql:"... on IssueComment"` // Label - LabeledEvent struct { - actorEvent - Label struct { - // Color githubv4.String - Name githubv4.String - } - } `graphql:"... on LabeledEvent"` - UnlabeledEvent struct { - actorEvent - Label struct { - // Color githubv4.String - Name githubv4.String - } - } `graphql:"... on UnlabeledEvent"` + LabeledEvent labeledEvent `graphql:"... on LabeledEvent"` + UnlabeledEvent unlabeledEvent `graphql:"... on UnlabeledEvent"` // Status ClosedEvent struct { @@ -145,11 +153,7 @@ type timelineItem struct { } `graphql:"... on ReopenedEvent"` // Title - RenamedTitleEvent struct { - actorEvent - CurrentTitle githubv4.String - PreviousTitle githubv4.String - } `graphql:"... on RenamedTitleEvent"` + RenamedTitleEvent renamedTitleEvent `graphql:"... on RenamedTitleEvent"` } type issueComment struct { @@ -160,14 +164,20 @@ type issueComment struct { UserContentEdits userContentEditConnection `graphql:"userContentEdits(last: $commentEditLast, before: $commentEditBefore)"` } +type userActor struct { + Name *githubv4.String + Email githubv4.String +} + type actor struct { Typename githubv4.String `graphql:"__typename"` Login githubv4.String AvatarUrl githubv4.String - User struct { - Name *githubv4.String - Email githubv4.String - } `graphql:"... on User"` + // User struct { + // Name *githubv4.String + // Email githubv4.String + // } `graphql:"... on User"` + User userActor `graphql:"... on User"` Organization struct { Name *githubv4.String Email *githubv4.String diff --git a/bridge/github/mocks/Client.go b/bridge/github/mocks/Client.go new file mode 100644 index 00000000..1270abb8 --- /dev/null +++ b/bridge/github/mocks/Client.go @@ -0,0 +1,44 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + githubv4 "github.com/shurcooL/githubv4" + + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// Mutate provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *Client) Mutate(_a0 context.Context, _a1 interface{}, _a2 githubv4.Input, _a3 map[string]interface{}) error { + ret := _m.Called(_a0, _a1, _a2, _a3) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, interface{}, githubv4.Input, map[string]interface{}) error); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Client) Query(_a0 context.Context, _a1 interface{}, _a2 map[string]interface{}) error { + ret := _m.Called(_a0, _a1, _a2) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, interface{}, map[string]interface{}) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} |