Tests for the github bridge (#706)

rng-dynamics created

Add integration test for github bridge

Change summary

bridge/github/import_integration_test.go | 384 ++++++++++++++++++++++++++
bridge/github/import_query.go            |  56 ++-
bridge/github/mocks/Client.go            |  44 ++
3 files changed, 461 insertions(+), 23 deletions(-)

Detailed changes

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()
+}

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

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
+}