0
0
mirror of https://github.com/thegeeklab/wp-github-comment.git synced 2024-11-21 13:50:40 +00:00

refactor: use dedicated github package (#115)

This commit is contained in:
Robert Kaussow 2024-05-11 10:25:19 +02:00 committed by GitHub
parent b0333e8700
commit 07d0b5405b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 646 additions and 324 deletions

View File

@ -95,3 +95,9 @@ run:
linters-settings: linters-settings:
gofumpt: gofumpt:
extra-rules: true extra-rules: true
issues:
exclude-rules:
- path: "_test.go"
linters:
- err113

6
.mockery.yaml Normal file
View File

@ -0,0 +1,6 @@
---
all: True
dir: "{{.PackageName}}/mocks"
outpkg: "mocks"
packages:
github.com/thegeeklab/wp-github-comment/github:

View File

@ -11,13 +11,14 @@ IMPORT := github.com/thegeeklab/$(EXECUTABLE)
GO ?= go GO ?= go
CWD ?= $(shell pwd) CWD ?= $(shell pwd)
PACKAGES ?= $(shell go list ./...) PACKAGES ?= $(shell go list ./... | grep -Ev '/mocks$$')
SOURCES ?= $(shell find . -name "*.go" -type f) SOURCES ?= $(shell find . -name "*.go" -type f)
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION) GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION)
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION) GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION)
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest
MOCKERY_PACKAGE ?= github.com/vektra/mockery/v2@latest
XGO_VERSION := go-1.22.x XGO_VERSION := go-1.22.x
XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64
@ -65,10 +66,7 @@ lint: golangci-lint
.PHONY: generate .PHONY: generate
generate: generate:
$(GO) generate $(PACKAGES) $(GO) generate $(PACKAGES)
$(GO) run $(MOCKERY_PACKAGE)
.PHONY: generate-docs
generate-docs:
$(GO) generate ./cmd/$(EXECUTABLE)/flags.go
.PHONY: test .PHONY: test
test: test:

41
github/api.go Normal file
View File

@ -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)
}

117
github/github.go Normal file
View File

@ -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<!-- id: %s -->\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("<!-- id: %s -->", i.Opt.Key)) {
return comment, nil
}
}
return nil, fmt.Errorf("%w: failed to find comment with key %s", ErrCommentNotFound, i.Opt.Key)
}

210
github/github_test.go Normal file
View File

@ -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("<!-- id: test-key -->\ntest comment\n")},
},
want: &github.IssueComment{
Body: github.String("<!-- id: test-key -->\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("<!-- id: test-key -->\ntest comment\n")},
{Body: github.String("another comment")},
},
want: &github.IssueComment{Body: github.String("<!-- id: test-key -->\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("<!-- id: test-key -->\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("<!-- id: test-key -->\ntest message\n")},
},
want: &github.IssueComment{
Body: github.String("<!-- id: test-key -->\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("<!-- id: test-key -->\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("<!-- id: %s -->\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("<!-- id: %s -->\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)
})
}
}

View File

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

5
go.mod
View File

@ -4,7 +4,6 @@ go 1.22
require ( require (
github.com/google/go-github/v61 v61.0.0 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/rs/zerolog v1.32.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/thegeeklab/wp-plugin-go/v2 v2.3.1 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/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.1.1 // 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/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect github.com/imdario/mergo v0.3.11 // indirect
github.com/joho/godotenv v1.5.1 // 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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // 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 github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )

10
go.sum
View File

@ -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.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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= 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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 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.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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= 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 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 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.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.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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 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.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.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/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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@ -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<!-- id: %s -->\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("<!-- id: %s -->", i.Key)) {
return comment, nil
}
}
return nil, fmt.Errorf("%w key: %s", ErrCommentNotFound, i.Key)
}

View File

@ -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 := "<!-- id: " + key + " -->"
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 := "<!-- id: " + key + " -->"
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<!-- id: test-key -->\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<!-- id: test-key -->\n")},
},
{
name: "update non-existing comment",
update: true,
want: &github.IssueComment{Body: github.String("test message\n<!-- id: test-key -->\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)
})
}
}

View File

@ -8,10 +8,9 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/google/go-github/v61/github"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
gh "github.com/thegeeklab/wp-github-comment/github"
"github.com/thegeeklab/wp-plugin-go/v2/file" "github.com/thegeeklab/wp-plugin-go/v2/file"
"golang.org/x/oauth2"
) )
var ErrPluginEventNotSupported = errors.New("event not supported") 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) return fmt.Errorf("validation failed: %w", err)
} }
//nolint:contextcheck
if err := p.Execute(); err != nil { if err := p.Execute(); err != nil {
return fmt.Errorf("execution failed: %w", err) return fmt.Errorf("execution failed: %w", err)
} }
@ -68,22 +66,15 @@ func (p *Plugin) Validate() error {
// Execute provides the implementation of the plugin. // Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error { func (p *Plugin) Execute() error {
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: p.Settings.APIKey}) client := gh.NewClient(p.Network.Context, p.Settings.baseURL, p.Settings.APIKey, p.Network.Client)
tc := oauth2.NewClient( client.Issue.Opt = gh.IssueOptions{
context.WithValue(p.Network.Context, oauth2.HTTPClient, p.Network.Client), Repo: p.Metadata.Repository.Name,
ts, Owner: p.Metadata.Repository.Owner,
) Message: p.Settings.Message,
Update: p.Settings.Update,
gh := github.NewClient(tc) Key: p.Settings.Key,
gh.BaseURL = p.Settings.baseURL Number: p.Metadata.Curr.PullRequest,
}
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
if p.Settings.SkipMissing && !p.Settings.IsFile { if p.Settings.SkipMissing && !p.Settings.IsFile {
log.Info(). log.Info().