diff --git a/.golangci.yml b/.golangci.yml index 5de88c8..451e5a0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -95,3 +95,9 @@ run: linters-settings: gofumpt: extra-rules: true + +issues: + exclude-rules: + - path: "_test.go" + linters: + - err113 diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..eb5c997 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,6 @@ +--- +all: True +dir: "{{.PackageName}}/mocks" +outpkg: "mocks" +packages: + github.com/thegeeklab/wp-github-comment/github: diff --git a/Makefile b/Makefile index 33614b1..8110a07 100644 --- a/Makefile +++ b/Makefile @@ -11,13 +11,14 @@ IMPORT := github.com/thegeeklab/$(EXECUTABLE) GO ?= go CWD ?= $(shell pwd) -PACKAGES ?= $(shell go list ./...) +PACKAGES ?= $(shell go list ./... | grep -Ev '/mocks$$') SOURCES ?= $(shell find . -name "*.go" -type f) GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION) GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION) XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest +MOCKERY_PACKAGE ?= github.com/vektra/mockery/v2@latest XGO_VERSION := go-1.22.x XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 @@ -65,10 +66,7 @@ lint: golangci-lint .PHONY: generate generate: $(GO) generate $(PACKAGES) - -.PHONY: generate-docs -generate-docs: - $(GO) generate ./cmd/$(EXECUTABLE)/flags.go + $(GO) run $(MOCKERY_PACKAGE) .PHONY: test test: diff --git a/github/api.go b/github/api.go new file mode 100644 index 0000000..20015e6 --- /dev/null +++ b/github/api.go @@ -0,0 +1,41 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v61/github" +) + +// APIClient is an interface that wraps the GitHub API client. +// +//nolint:lll +type IssueService interface { + CreateComment(ctx context.Context, owner, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) + EditComment(ctx context.Context, owner, repo string, commentID int64, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) + ListComments(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) +} + +type IssueServiceImpl struct { + client *github.Client +} + +// CreateComment wraps the CreateComment method of the github.IssuesService. +// +//nolint:lll +func (s *IssueServiceImpl) CreateComment(ctx context.Context, owner, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + return s.client.Issues.CreateComment(ctx, owner, repo, number, comment) +} + +// EditComment wraps the EditComment method of the github.IssuesService. +// +//nolint:lll +func (s *IssueServiceImpl) EditComment(ctx context.Context, owner, repo string, commentID int64, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + return s.client.Issues.EditComment(ctx, owner, repo, commentID, comment) +} + +// ListComments wraps the ListComments method of the github.IssuesService. +// +//nolint:lll +func (s *IssueServiceImpl) ListComments(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { + return s.client.Issues.ListComments(ctx, owner, repo, number, opts) +} diff --git a/github/github.go b/github/github.go new file mode 100644 index 0000000..ebafab0 --- /dev/null +++ b/github/github.go @@ -0,0 +1,117 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/google/go-github/v61/github" + "golang.org/x/oauth2" +) + +var ErrCommentNotFound = errors.New("comment not found") + +type Client struct { + client *github.Client + Issue *Issue +} + +type Issue struct { + client IssueService + Opt IssueOptions +} + +type IssueOptions struct { + Number int + Message string + Key string + Repo string + Owner string + Update bool +} + +// NewGitHubClient creates a new GitHubClient instance that wraps the provided GitHub API client. +// The GitHubClient provides a higher-level interface for interacting with the GitHub API, +// including methods for managing GitHub issues. +func NewClient(ctx context.Context, url *url.URL, token string, client *http.Client) *Client { + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient( + context.WithValue(ctx, oauth2.HTTPClient, client), + ts, + ) + + c := github.NewClient(tc) + c.BaseURL = url + + return &Client{ + client: c, + Issue: &Issue{ + client: &IssueServiceImpl{client: c}, + Opt: IssueOptions{}, + }, + } +} + +// AddComment adds a new comment or updates an existing comment on a GitHub issue. +// If the Update field is true, it will append a unique identifier to the comment +// body and attempt to find and update the existing comment with that identifier. +// Otherwise, it will create a new comment on the issue. +func (i *Issue) AddComment(ctx context.Context) (*github.IssueComment, error) { + issueComment := &github.IssueComment{ + Body: &i.Opt.Message, + } + + if i.Opt.Update { + // Append plugin comment ID to comment message so we can search for it later + *issueComment.Body = fmt.Sprintf("%s\n\n", i.Opt.Message, i.Opt.Key) + + comment, err := i.FindComment(ctx) + if err != nil && !errors.Is(err, ErrCommentNotFound) { + return nil, err + } + + if comment != nil { + comment, _, err = i.client.EditComment(ctx, i.Opt.Owner, i.Opt.Repo, *comment.ID, issueComment) + + return comment, err + } + } + + comment, _, err := i.client.CreateComment(ctx, i.Opt.Owner, i.Opt.Repo, i.Opt.Number, issueComment) + + return comment, err +} + +// FindComment returns the GitHub issue comment that contains the specified key, or nil if no such comment exists. +// It retrieves all comments on the issue and searches for one that contains the specified key in the comment body. +func (i *Issue) FindComment(ctx context.Context) (*github.IssueComment, error) { + var allComments []*github.IssueComment + + opts := &github.IssueListCommentsOptions{} + + for { + comments, resp, err := i.client.ListComments(ctx, i.Opt.Owner, i.Opt.Repo, i.Opt.Number, opts) + if err != nil { + return nil, err + } + + allComments = append(allComments, comments...) + + if resp == nil || resp.NextPage == 0 { + break + } + + opts.Page = resp.NextPage + } + + for _, comment := range allComments { + if strings.Contains(*comment.Body, fmt.Sprintf("", i.Opt.Key)) { + return comment, nil + } + } + + return nil, fmt.Errorf("%w: failed to find comment with key %s", ErrCommentNotFound, i.Opt.Key) +} diff --git a/github/github_test.go b/github/github_test.go new file mode 100644 index 0000000..e47ed47 --- /dev/null +++ b/github/github_test.go @@ -0,0 +1,210 @@ +package github + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/google/go-github/v61/github" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/thegeeklab/wp-github-comment/github/mocks" +) + +func TestGithubIssue_FindComment(t *testing.T) { + tests := []struct { + name string + issueOpt IssueOptions + comments []*github.IssueComment + want *github.IssueComment + wantErr error + }{ + { + name: "no comments", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + }, + wantErr: ErrCommentNotFound, + }, + { + name: "comment found", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + }, + comments: []*github.IssueComment{ + {Body: github.String("\ntest comment\n")}, + }, + want: &github.IssueComment{ + Body: github.String("\ntest comment\n"), + }, + }, + { + name: "comment not found", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + }, + comments: []*github.IssueComment{ + {Body: github.String("other comment")}, + }, + wantErr: ErrCommentNotFound, + }, + { + name: "multiple comments", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + }, + comments: []*github.IssueComment{ + {Body: github.String("other comment")}, + {Body: github.String("\ntest comment\n")}, + {Body: github.String("another comment")}, + }, + want: &github.IssueComment{Body: github.String("\ntest comment\n")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := mocks.NewMockIssueService(t) + issue := &Issue{ + client: mockClient, + Opt: tt.issueOpt, + } + + mockClient. + On("ListComments", mock.Anything, tt.issueOpt.Owner, tt.issueOpt.Repo, mock.Anything, mock.Anything). + Return(tt.comments, nil, nil) + + got, err := issue.FindComment(context.Background()) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.want, got) + + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGithubIssue_AddComment(t *testing.T) { + tests := []struct { + name string + issueOpt IssueOptions + comments []*github.IssueComment + want *github.IssueComment + wantErr error + }{ + { + name: "create new comment", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + Message: "test message", + Update: false, + }, + want: &github.IssueComment{ + Body: github.String("\ntest message\n"), + }, + }, + { + name: "update existing comment", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + Message: "test message", + Update: true, + }, + comments: []*github.IssueComment{ + {ID: github.Int64(123), Body: github.String("\ntest message\n")}, + }, + want: &github.IssueComment{ + Body: github.String("\ntest message\n"), + }, + }, + { + name: "update non-existing comment", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + Message: "test message", + Update: true, + }, + want: &github.IssueComment{ + Body: github.String("\ntest message\n"), + }, + }, + { + name: "create new comment with error", + issueOpt: IssueOptions{ + Key: "test-key", + Owner: "test-owner", + Repo: "test-repo", + Message: "test message", + Update: false, + }, + wantErr: errors.New("internal server error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := mocks.NewMockIssueService(t) + issue := &Issue{ + client: mockClient, + Opt: tt.issueOpt, + } + + if tt.issueOpt.Update { + mockClient. + On("ListComments", mock.Anything, tt.issueOpt.Owner, tt.issueOpt.Repo, mock.Anything, mock.Anything). + Return(tt.comments, nil, nil) + } + + if tt.issueOpt.Update && tt.comments != nil { + mockClient. + On("EditComment", mock.Anything, tt.issueOpt.Owner, tt.issueOpt.Repo, mock.Anything, mock.Anything). + Return(&github.IssueComment{ + Body: github.String(fmt.Sprintf("\n%s\n", tt.issueOpt.Key, tt.issueOpt.Message)), + }, nil, nil) + } + + if tt.comments == nil { + var comment *github.IssueComment + if tt.wantErr == nil { + comment = &github.IssueComment{ + Body: github.String(fmt.Sprintf("\n%s\n", tt.issueOpt.Key, tt.issueOpt.Message)), + } + } + + mockClient. + On("CreateComment", mock.Anything, tt.issueOpt.Owner, tt.issueOpt.Repo, mock.Anything, mock.Anything). + Return(comment, nil, tt.wantErr) + } + + got, err := issue.AddComment(context.Background()) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.want, got) + + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/github/mocks/mock_IssueService.go b/github/mocks/mock_IssueService.go new file mode 100644 index 0000000..429887e --- /dev/null +++ b/github/mocks/mock_IssueService.go @@ -0,0 +1,250 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + github "github.com/google/go-github/v61/github" + mock "github.com/stretchr/testify/mock" +) + +// MockIssueService is an autogenerated mock type for the IssueService type +type MockIssueService struct { + mock.Mock +} + +type MockIssueService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockIssueService) EXPECT() *MockIssueService_Expecter { + return &MockIssueService_Expecter{mock: &_m.Mock} +} + +// CreateComment provides a mock function with given fields: ctx, owner, repo, number, comment +func (_m *MockIssueService) CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + ret := _m.Called(ctx, owner, repo, number, comment) + + if len(ret) == 0 { + panic("no return value specified for CreateComment") + } + + var r0 *github.IssueComment + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, *github.IssueComment) (*github.IssueComment, *github.Response, error)); ok { + return rf(ctx, owner, repo, number, comment) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, *github.IssueComment) *github.IssueComment); ok { + r0 = rf(ctx, owner, repo, number, comment) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.IssueComment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int, *github.IssueComment) *github.Response); ok { + r1 = rf(ctx, owner, repo, number, comment) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, int, *github.IssueComment) error); ok { + r2 = rf(ctx, owner, repo, number, comment) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockIssueService_CreateComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateComment' +type MockIssueService_CreateComment_Call struct { + *mock.Call +} + +// CreateComment is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repo string +// - number int +// - comment *github.IssueComment +func (_e *MockIssueService_Expecter) CreateComment(ctx interface{}, owner interface{}, repo interface{}, number interface{}, comment interface{}) *MockIssueService_CreateComment_Call { + return &MockIssueService_CreateComment_Call{Call: _e.mock.On("CreateComment", ctx, owner, repo, number, comment)} +} + +func (_c *MockIssueService_CreateComment_Call) Run(run func(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment)) *MockIssueService_CreateComment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int), args[4].(*github.IssueComment)) + }) + return _c +} + +func (_c *MockIssueService_CreateComment_Call) Return(_a0 *github.IssueComment, _a1 *github.Response, _a2 error) *MockIssueService_CreateComment_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockIssueService_CreateComment_Call) RunAndReturn(run func(context.Context, string, string, int, *github.IssueComment) (*github.IssueComment, *github.Response, error)) *MockIssueService_CreateComment_Call { + _c.Call.Return(run) + return _c +} + +// EditComment provides a mock function with given fields: ctx, owner, repo, commentID, comment +func (_m *MockIssueService) EditComment(ctx context.Context, owner string, repo string, commentID int64, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + ret := _m.Called(ctx, owner, repo, commentID, comment) + + if len(ret) == 0 { + panic("no return value specified for EditComment") + } + + var r0 *github.IssueComment + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64, *github.IssueComment) (*github.IssueComment, *github.Response, error)); ok { + return rf(ctx, owner, repo, commentID, comment) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64, *github.IssueComment) *github.IssueComment); ok { + r0 = rf(ctx, owner, repo, commentID, comment) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.IssueComment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int64, *github.IssueComment) *github.Response); ok { + r1 = rf(ctx, owner, repo, commentID, comment) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, int64, *github.IssueComment) error); ok { + r2 = rf(ctx, owner, repo, commentID, comment) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockIssueService_EditComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EditComment' +type MockIssueService_EditComment_Call struct { + *mock.Call +} + +// EditComment is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repo string +// - commentID int64 +// - comment *github.IssueComment +func (_e *MockIssueService_Expecter) EditComment(ctx interface{}, owner interface{}, repo interface{}, commentID interface{}, comment interface{}) *MockIssueService_EditComment_Call { + return &MockIssueService_EditComment_Call{Call: _e.mock.On("EditComment", ctx, owner, repo, commentID, comment)} +} + +func (_c *MockIssueService_EditComment_Call) Run(run func(ctx context.Context, owner string, repo string, commentID int64, comment *github.IssueComment)) *MockIssueService_EditComment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int64), args[4].(*github.IssueComment)) + }) + return _c +} + +func (_c *MockIssueService_EditComment_Call) Return(_a0 *github.IssueComment, _a1 *github.Response, _a2 error) *MockIssueService_EditComment_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockIssueService_EditComment_Call) RunAndReturn(run func(context.Context, string, string, int64, *github.IssueComment) (*github.IssueComment, *github.Response, error)) *MockIssueService_EditComment_Call { + _c.Call.Return(run) + return _c +} + +// ListComments provides a mock function with given fields: ctx, owner, repo, number, opts +func (_m *MockIssueService) ListComments(ctx context.Context, owner string, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { + ret := _m.Called(ctx, owner, repo, number, opts) + + if len(ret) == 0 { + panic("no return value specified for ListComments") + } + + var r0 []*github.IssueComment + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error)); ok { + return rf(ctx, owner, repo, number, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, *github.IssueListCommentsOptions) []*github.IssueComment); ok { + r0 = rf(ctx, owner, repo, number, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*github.IssueComment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int, *github.IssueListCommentsOptions) *github.Response); ok { + r1 = rf(ctx, owner, repo, number, opts) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, int, *github.IssueListCommentsOptions) error); ok { + r2 = rf(ctx, owner, repo, number, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockIssueService_ListComments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListComments' +type MockIssueService_ListComments_Call struct { + *mock.Call +} + +// ListComments is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repo string +// - number int +// - opts *github.IssueListCommentsOptions +func (_e *MockIssueService_Expecter) ListComments(ctx interface{}, owner interface{}, repo interface{}, number interface{}, opts interface{}) *MockIssueService_ListComments_Call { + return &MockIssueService_ListComments_Call{Call: _e.mock.On("ListComments", ctx, owner, repo, number, opts)} +} + +func (_c *MockIssueService_ListComments_Call) Run(run func(ctx context.Context, owner string, repo string, number int, opts *github.IssueListCommentsOptions)) *MockIssueService_ListComments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int), args[4].(*github.IssueListCommentsOptions)) + }) + return _c +} + +func (_c *MockIssueService_ListComments_Call) Return(_a0 []*github.IssueComment, _a1 *github.Response, _a2 error) *MockIssueService_ListComments_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockIssueService_ListComments_Call) RunAndReturn(run func(context.Context, string, string, int, *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error)) *MockIssueService_ListComments_Call { + _c.Call.Return(run) + return _c +} + +// NewMockIssueService creates a new instance of MockIssueService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockIssueService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockIssueService { + mock := &MockIssueService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/go.mod b/go.mod index 91793ae..6692f9f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.22 require ( github.com/google/go-github/v61 v61.0.0 - github.com/migueleliasweb/go-github-mock v0.0.23 github.com/rs/zerolog v1.32.0 github.com/stretchr/testify v1.9.0 github.com/thegeeklab/wp-plugin-go/v2 v2.3.1 @@ -18,10 +17,8 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/go-github/v59 v59.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.1.1 // indirect - github.com/gorilla/mux v1.8.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/joho/godotenv v1.5.1 // indirect @@ -33,10 +30,10 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/time v0.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e08ef00..66d1023 100644 --- a/go.sum +++ b/go.sum @@ -15,16 +15,12 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v59 v59.0.0 h1:7h6bgpF5as0YQLLkEiVqpgtJqjimMYhBkD4jT5aN3VA= -github.com/google/go-github/v59 v59.0.0/go.mod h1:rJU4R0rQHFVFDOkqGWxfLNo6vEk4dv40oDjhV/gH6wM= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= @@ -36,8 +32,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/migueleliasweb/go-github-mock v0.0.23 h1:GOi9oX/+Seu9JQ19V8bPDLqDI7M9iEOjo3g8v1k6L2c= -github.com/migueleliasweb/go-github-mock v0.0.23/go.mod h1:NsT8FGbkvIZQtDu38+295sZEX8snaUiiQgsGxi6GUxk= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -55,6 +49,8 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -100,8 +96,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/plugin/github.go b/plugin/github.go deleted file mode 100644 index c1f57df..0000000 --- a/plugin/github.go +++ /dev/null @@ -1,96 +0,0 @@ -package plugin - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/google/go-github/v61/github" -) - -var ErrCommentNotFound = errors.New("no comment found") - -type GithubClient struct { - Client *github.Client - Issue *GithubIssue -} - -type GithubIssue struct { - *github.Client - Number int - Message string - Key string - Repo string - Owner string - Update bool -} - -// Constructor function for Parent. -func NewGithubClient(client *github.Client) *GithubClient { - return &GithubClient{ - Client: client, - Issue: &GithubIssue{Client: client}, - } -} - -// AddComment adds a new comment or updates an existing comment on a GitHub issue. -// If the Update field is true, it will append a unique identifier to the comment -// body and attempt to find and update the existing comment with that identifier. -// Otherwise, it will create a new comment on the issue. -func (i *GithubIssue) AddComment(ctx context.Context) (*github.IssueComment, error) { - issueComment := &github.IssueComment{ - Body: &i.Message, - } - - if i.Update { - // Append plugin comment ID to comment message so we can search for it later - *issueComment.Body = fmt.Sprintf("%s\n\n", i.Message, i.Key) - - comment, err := i.FindComment(ctx) - if err != nil && !errors.Is(err, ErrCommentNotFound) { - return comment, err - } - - if comment != nil { - comment, _, err = i.Client.Issues.EditComment(ctx, i.Owner, i.Repo, *comment.ID, issueComment) - - return comment, err - } - } - - comment, _, err := i.Client.Issues.CreateComment(ctx, i.Owner, i.Repo, i.Number, issueComment) - - return comment, err -} - -// FindComment returns the GitHub issue comment that contains the specified key, or nil if no such comment exists. -// It retrieves all comments on the issue and searches for one that contains the specified key in the comment body. -func (i *GithubIssue) FindComment(ctx context.Context) (*github.IssueComment, error) { - var allComments []*github.IssueComment - - opts := &github.IssueListCommentsOptions{} - - for { - comments, resp, err := i.Client.Issues.ListComments(ctx, i.Owner, i.Repo, i.Number, opts) - if err != nil { - return nil, err - } - - allComments = append(allComments, comments...) - - if resp.NextPage == 0 { - break - } - - opts.Page = resp.NextPage - } - - for _, comment := range allComments { - if strings.Contains(*comment.Body, fmt.Sprintf("", i.Key)) { - return comment, nil - } - } - - return nil, fmt.Errorf("%w key: %s", ErrCommentNotFound, i.Key) -} diff --git a/plugin/github_test.go b/plugin/github_test.go deleted file mode 100644 index 49bfc63..0000000 --- a/plugin/github_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - "net/http" - "testing" - - "github.com/google/go-github/v61/github" - "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/stretchr/testify/assert" -) - -func TestGithubIssue_FindComment(t *testing.T) { - ctx := context.Background() - key := "test-key" - keyPattern := "" - owner := "test-owner" - repo := "test-repo" - number := 1 - - tests := []struct { - name string - comments []*github.IssueComment - want *github.IssueComment - wantErr error - }{ - { - name: "no comments", - want: nil, - wantErr: ErrCommentNotFound, - }, - { - name: "comment found", - comments: []*github.IssueComment{ - {Body: github.String(keyPattern)}, - }, - want: &github.IssueComment{Body: github.String(keyPattern)}, - }, - { - name: "comment not found", - comments: []*github.IssueComment{ - {Body: github.String("other comment")}, - }, - want: nil, - wantErr: ErrCommentNotFound, - }, - { - name: "multiple comments", - comments: []*github.IssueComment{ - {Body: github.String("other comment")}, - {Body: github.String(keyPattern)}, - {Body: github.String("another comment")}, - }, - want: &github.IssueComment{Body: github.String(keyPattern)}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockedHTTPClient := mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, - tt.comments, - ), - ) - - client := github.NewClient(mockedHTTPClient) - issue := &GithubIssue{ - Client: client, - Owner: owner, - Repo: repo, - Number: number, - Key: key, - } - - got, err := issue.FindComment(ctx) - if tt.wantErr != nil { - assert.Error(t, err) - assert.Equal(t, tt.want, got) - - return - } - - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestGithubIssue_AddComment(t *testing.T) { - ctx := context.Background() - key := "test-key" - keyPattern := "" - owner := "test-owner" - repo := "test-repo" - number := 1 - message := "test message" - - tests := []struct { - name string - update bool - existingKey string - comments []*github.IssueComment - want *github.IssueComment - wantErr bool - }{ - { - name: "create new comment", - update: false, - want: &github.IssueComment{Body: github.String("test message\n\n")}, - }, - { - name: "update existing comment", - update: true, - comments: []*github.IssueComment{ - {ID: github.Int64(123), Body: github.String(keyPattern)}, - }, - want: &github.IssueComment{Body: github.String("test message\n\n")}, - }, - { - name: "update non-existing comment", - update: true, - want: &github.IssueComment{Body: github.String("test message\n\n")}, - }, - { - name: "create new comment with error", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockedHTTPClient := mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, - tt.comments, - ), - mock.WithRequestMatchHandler( - mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - if tt.wantErr { - mock.WriteError(w, http.StatusInternalServerError, "internal server error") - } else { - _, _ = w.Write(mock.MustMarshal( - &github.IssueComment{ - Body: github.String(fmt.Sprintf("%s\n%s\n", message, keyPattern)), - }, - )) - } - }), - ), - mock.WithRequestMatchHandler( - mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - if tt.wantErr { - mock.WriteError(w, http.StatusInternalServerError, "internal server error") - } else { - _, _ = w.Write(mock.MustMarshal( - &github.IssueComment{ - Body: github.String(fmt.Sprintf("%s\n%s\n", message, keyPattern)), - }, - )) - } - }), - ), - ) - - client := github.NewClient(mockedHTTPClient) - issue := &GithubIssue{ - Client: client, - Owner: owner, - Repo: repo, - Number: number, - Key: key, - Message: message, - Update: tt.update, - } - - got, err := issue.AddComment(ctx) - if tt.wantErr { - assert.Error(t, err) - assert.Equal(t, tt.want, got) - - return - } - - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/plugin/impl.go b/plugin/impl.go index b0a857c..67e3687 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -8,10 +8,9 @@ import ( "net/url" "strings" - "github.com/google/go-github/v61/github" "github.com/rs/zerolog/log" + gh "github.com/thegeeklab/wp-github-comment/github" "github.com/thegeeklab/wp-plugin-go/v2/file" - "golang.org/x/oauth2" ) var ErrPluginEventNotSupported = errors.New("event not supported") @@ -22,7 +21,6 @@ func (p *Plugin) run(ctx context.Context) error { return fmt.Errorf("validation failed: %w", err) } - //nolint:contextcheck if err := p.Execute(); err != nil { return fmt.Errorf("execution failed: %w", err) } @@ -68,22 +66,15 @@ func (p *Plugin) Validate() error { // Execute provides the implementation of the plugin. func (p *Plugin) Execute() error { - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: p.Settings.APIKey}) - tc := oauth2.NewClient( - context.WithValue(p.Network.Context, oauth2.HTTPClient, p.Network.Client), - ts, - ) - - gh := github.NewClient(tc) - gh.BaseURL = p.Settings.baseURL - - client := NewGithubClient(gh) - client.Issue.Repo = p.Metadata.Repository.Name - client.Issue.Owner = p.Metadata.Repository.Owner - client.Issue.Message = p.Settings.Message - client.Issue.Update = p.Settings.Update - client.Issue.Key = p.Settings.Key - client.Issue.Number = p.Metadata.Curr.PullRequest + client := gh.NewClient(p.Network.Context, p.Settings.baseURL, p.Settings.APIKey, p.Network.Client) + client.Issue.Opt = gh.IssueOptions{ + Repo: p.Metadata.Repository.Name, + Owner: p.Metadata.Repository.Owner, + Message: p.Settings.Message, + Update: p.Settings.Update, + Key: p.Settings.Key, + Number: p.Metadata.Curr.PullRequest, + } if p.Settings.SkipMissing && !p.Settings.IsFile { log.Info().