diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cd8732a --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +BINARY_NAME=pipescope + +build: + CGO_ENABLED=0 go build -mod=mod -o ./targets/${BINARY_NAME} main.go + +mod: + go mod download + +run: build + ./${BINARY_NAME} + +clean: + go clean -testcache + rm -rf ./targets + +test: + go test -race -v ./... + +test_coverage: + go test ./... -coverprofile=coverage.out + +dep: + go get . + +vet: + go vet + +lint: + golangci-lint run -v \ No newline at end of file diff --git a/README.md b/README.md index b28c06e..b80437b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Output: ## Limitations/Roadmap - Project is currently experimental so please do not use this in production anywhere -- Needs CI/CD, tests, and a Makefile +- Needs CI/CD, tests, and a Makefile - Currently, PipeScope can only monitor GitLab pipelines. Future versions will extend this to include GitHub workflows. - There is not a lot of flexibility to select pipelines or projects via command-line input -- this should be changed to allow custom pipeline IDs to be specified. - Include more information on pipeline jobs diff --git a/go.mod b/go.mod index 7922287..30c0ef0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/gen2brain/beeep v0.0.0-20240112042604-c7bb2cd88fea + github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-github/v61 v61.0.0 github.com/xanzy/go-gitlab v0.102.0 @@ -17,7 +18,6 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/internal/checker/service.go b/internal/checker/service.go index 62926cd..9d20010 100644 --- a/internal/checker/service.go +++ b/internal/checker/service.go @@ -10,10 +10,10 @@ import ( type Service struct { gatewayClient gateway.Client - gitClient *git.Client + gitClient git.Client } -func New(gw gateway.Client, gc *git.Client) *Service { +func New(gw gateway.Client, gc git.Client) *Service { return &Service{ gatewayClient: gw, gitClient: gc, diff --git a/internal/checker/service_test.go b/internal/checker/service_test.go new file mode 100644 index 0000000..d83db7f --- /dev/null +++ b/internal/checker/service_test.go @@ -0,0 +1,121 @@ +package checker + +import ( + "sync" + "testing" + "time" + + "github.com/gregfurman/pipescope/internal/gateway" +) + +type providerMock struct { + mockedGetPipelineBySha func(id, sha string) (*gateway.Pipeline, error) + mockedGetPipeline func(id string, pid int) (*gateway.Pipeline, error) + mockedIsStatusPending func(status string) bool + lock sync.Mutex +} + +func (pm *providerMock) GetPipelineBySha(id, sha string) (*gateway.Pipeline, error) { + return pm.mockedGetPipelineBySha(id, sha) +} + +func (pm *providerMock) GetPipeline(id string, pid int) (*gateway.Pipeline, error) { + pm.lock.Lock() + pipeline, err := pm.mockedGetPipeline(id, pid) + pm.lock.Unlock() + + return pipeline, err +} + +func (pm *providerMock) IsStatusPending(status string) bool { + pm.lock.Lock() + flag := pm.mockedIsStatusPending(status) + pm.lock.Unlock() + return flag +} + +type gitMock struct { + mockedGetHead func() (string, error) + mockedGetRemoteURL func() (string, error) +} + +func (gm *gitMock) GetHead() (string, error) { + return gm.mockedGetHead() +} +func (gm *gitMock) GetRemoteURL() (string, error) { + return gm.mockedGetRemoteURL() +} + +func Test_Service(t *testing.T) { + expectedPipeline := gateway.Pipeline{ + ID: 1, + ProjectID: "PROJECT_ID", + Status: "pending", + URL: "www.example.com/repo/owner/pipelines/1", + CommitSha: "COMMIT_SHA", + } + + providerClient := &providerMock{ + mockedGetPipelineBySha: func(id, sha string) (*gateway.Pipeline, error) { return &expectedPipeline, nil }, + mockedGetPipeline: func(id string, pid int) (*gateway.Pipeline, error) { return &expectedPipeline, nil }, + mockedIsStatusPending: func(status string) bool { return true }, + lock: sync.Mutex{}, + } + + gitClient := &gitMock{ + mockedGetHead: func() (string, error) { return "COMMIT_SHA", nil }, + mockedGetRemoteURL: func() (string, error) { return "www.example.com/repo/owner", nil }, + } + + svc := New(providerClient, gitClient) + + pipeline, err := svc.GetPipeline() + if err != nil { + t.Error("did not expect error") + } + + if *pipeline != expectedPipeline { + t.Errorf("expected pipeline object %v, got %v", *pipeline, expectedPipeline) + } + + status, err := svc.GetPipelineStatus() + if err != nil { + t.Error("did not expect error") + } + + if status != pipeline.Status { + t.Errorf("expected pipeline status %v, got %v", *pipeline, expectedPipeline) + } + + // Poll every 500ms + statusCh, _ := svc.PollPipelineStatus("PROJECT_ID", 1, 500*time.Millisecond) + + // Change the status to "success" after 1000ms + time.AfterFunc(1000*time.Millisecond, func() { + providerClient.lock.Lock() + providerClient.mockedIsStatusPending = func(status string) bool { return false } + providerClient.mockedGetPipeline = func(id string, pid int) (*gateway.Pipeline, error) { + return &gateway.Pipeline{ + ID: 1, + ProjectID: "PROJECT_ID", + Status: "success", + URL: "www.example.com/repo/owner/pipelines/1", + CommitSha: "COMMIT_SHA", + }, nil + } + providerClient.lock.Unlock() + }) + + var gotStatus string + for s := range statusCh { + gotStatus = s + if svc.gatewayClient.IsStatusPending(gotStatus) && gotStatus != "pending" { + t.Errorf("expected status to be pending, got %s", gotStatus) + } + } + + if gotStatus != "success" { + t.Errorf("expected status to be success, got %s", gotStatus) + } + +} diff --git a/internal/gateway/factory.go b/internal/gateway/factory.go index ad38cfe..f52a365 100644 --- a/internal/gateway/factory.go +++ b/internal/gateway/factory.go @@ -5,7 +5,10 @@ import ( "strings" ) -func New(token string, t ProviderType) (Client, error) { //nolint:ireturn +func New(token string, t ProviderType) ( //nolint:ireturn + Client, + error, +) { switch t { case GitHub: return NewGitHubClient(token) @@ -16,7 +19,10 @@ func New(token string, t ProviderType) (Client, error) { //nolint:ireturn } } -func NewFromToken(token string) (Client, error) { //nolint:ireturn +func NewFromToken(token string) ( //nolint:ireturn + Client, + error, +) { switch { case isGitHubToken(token): return NewGitHubClient(token) @@ -28,7 +34,10 @@ func NewFromToken(token string) (Client, error) { //nolint:ireturn } } -func NewFromRemoteURL(token, remoteURL string) (Client, error) { //nolint:ireturn +func NewFromRemoteURL(token, remoteURL string) ( //nolint:ireturn + Client, + error, +) { switch { case strings.Contains(remoteURL, "github.com"): return NewGitHubClient(token) diff --git a/internal/gateway/github_client.go b/internal/gateway/github_client.go index ea94a75..b007d9b 100644 --- a/internal/gateway/github_client.go +++ b/internal/gateway/github_client.go @@ -2,6 +2,7 @@ package gateway import ( "context" + "errors" "fmt" "strings" @@ -66,6 +67,10 @@ func (c *GitHubClient) GetPipeline(id string, pid int) (*Pipeline, error) { return nil, fmt.Errorf("failed to retrieve pipeline from GitHub: %w", err) } + if workflow == nil || workflow.ID == nil { + return nil, errors.New("no workflows found") + } + return workflowToPipeline(workflow), nil } @@ -80,7 +85,7 @@ func workflowToPipeline(wf *github.WorkflowRun) *Pipeline { ID: int(wf.GetID()), ProjectID: wf.GetRepository().GetFullName(), URL: wf.GetHTMLURL(), - CommitSha: wf.GetHeadCommit().GetSHA(), + CommitSha: wf.GetHeadSHA(), } } diff --git a/internal/gateway/github_client_test.go b/internal/gateway/github_client_test.go new file mode 100644 index 0000000..754aa1f --- /dev/null +++ b/internal/gateway/github_client_test.go @@ -0,0 +1,266 @@ +package gateway + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-github/v61/github" +) + +type mockRoundTripper struct { + response *http.Response +} + +func (rt *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt.response, nil +} + +func makeJSONResponse(body string) *http.Response { + recorder := httptest.NewRecorder() + recorder.Header().Add("Content-Type", "application/json") + recorder.WriteString(body) + return recorder.Result() +} + +func Test_GitHub_GetProjectID(t *testing.T) { + + tests := []struct { + name string + mockedResponseBody string + + path string + + want int + wantErr bool + }{ + {name: "Success", mockedResponseBody: `{"id":12345}`, want: 12345, path: "owner/repo"}, + {name: "Fails due to malformed path", wantErr: true, path: "incorrect path format"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(ts *testing.T) { + mockedAPI := github.NewClient(&http.Client{Transport: &mockRoundTripper{makeJSONResponse(tt.mockedResponseBody)}}) + + client := GitHubClient{ + api: mockedAPI, + } + + got, err := client.GetProjectID(tt.path) + if tt.wantErr { + if err == nil { + ts.Error("expected an error, got nil") + } + return + } + + if tt.want != got { + ts.Errorf("expected %d, got %d", tt.want, got) + } + + }) + } + +} + +func Test_GitHub_GetPipelineBySha(t *testing.T) { + + tests := []struct { + name string + mockedResponseBody string + + path string + sha string + + want Pipeline + wantErr bool + }{ + { + name: "Successfully returns in_progress pipeline", + mockedResponseBody: `{"total_count":1, "workflow_runs": [ + { + "id": 8858984663, + "head_branch": "test-workflow", + "head_sha": "23ebbb3b14c9a026199474d2931bdc55863dfffc", + "event": "push", + "status": "in_progress", + "workflow_id": 95717003, + "html_url": "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + "repository": { + "full_name": "gregfurman/pipescope" + } + } + ]}`, + sha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + path: "https://github.com/gregfurman/pipescope", + want: Pipeline{ + ID: 8858984663, + ProjectID: "gregfurman/pipescope", + CommitSha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + Status: "in_progress", + URL: "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + }, + }, + { + name: "Successfully returns completed and failed workflow", + mockedResponseBody: `{"total_count":1, "workflow_runs": [ + { + "id": 8858984663, + "head_branch": "test-workflow", + "head_sha": "23ebbb3b14c9a026199474d2931bdc55863dfffc", + "event": "push", + "status": "completed", + "conclusion": "failure", + "workflow_id": 95717003, + "html_url": "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + "repository": { + "full_name": "gregfurman/pipescope" + } + } + ]}`, + sha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + path: "https://github.com/gregfurman/pipescope", + want: Pipeline{ + ID: 8858984663, + ProjectID: "gregfurman/pipescope", + CommitSha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + Status: "failure", + URL: "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + }, + }, + { + name: "Fails due to no workflows found", + mockedResponseBody: `{"total_count":0, "workflow_runs": []}`, + sha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + path: "https://github.com/gregfurman/pipescope", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(ts *testing.T) { + mockedAPI := github.NewClient(&http.Client{Transport: &mockRoundTripper{makeJSONResponse(tt.mockedResponseBody)}}) + + client := GitHubClient{ + api: mockedAPI, + } + got, err := client.GetPipelineBySha(tt.path, tt.sha) + if err != nil && !tt.wantErr { + ts.Errorf("unexpected error occurred. expected nil, got %s", err) + return + } + + if tt.wantErr { + if err == nil { + ts.Error("expected an error, got nil") + } + return + } + + if tt.want != *got { + ts.Errorf("expected %v, got %v", tt.want, got) + } + + }) + } + +} + +func Test_GitHub_GetPipeline(t *testing.T) { + + tests := []struct { + name string + mockedResponseBody string + + path string + workflowID int + + want Pipeline + wantErr bool + }{ + { + name: "Successfully returns in_progress pipeline", + mockedResponseBody: `{ + "id": 8858984663, + "head_branch": "test-workflow", + "head_sha": "23ebbb3b14c9a026199474d2931bdc55863dfffc", + "event": "push", + "status": "in_progress", + "workflow_id": 95717003, + "html_url": "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + "repository": { + "full_name": "gregfurman/pipescope" + } + }`, + workflowID: 8858984663, + path: "https://github.com/gregfurman/pipescope", + want: Pipeline{ + ID: 8858984663, + ProjectID: "gregfurman/pipescope", + CommitSha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + Status: "in_progress", + URL: "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + }, + }, + { + name: "Successfully returns completed and failed workflow", + mockedResponseBody: `{ + "id": 8858984663, + "head_branch": "test-workflow", + "head_sha": "23ebbb3b14c9a026199474d2931bdc55863dfffc", + "event": "push", + "status": "completed", + "conclusion": "failure", + "workflow_id": 95717003, + "html_url": "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + "repository": { + "full_name": "gregfurman/pipescope" + } + }`, + workflowID: 8858984663, + path: "https://github.com/gregfurman/pipescope", + want: Pipeline{ + ID: 8858984663, + ProjectID: "gregfurman/pipescope", + CommitSha: "23ebbb3b14c9a026199474d2931bdc55863dfffc", + Status: "failure", + URL: "https://github.com/gregfurman/pipescope/actions/runs/8858984663", + }, + }, + { + name: "Fails due to no workflows found", + mockedResponseBody: `{}`, + workflowID: 8858984663, + path: "https://github.com/gregfurman/pipescope", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(ts *testing.T) { + mockedAPI := github.NewClient(&http.Client{Transport: &mockRoundTripper{makeJSONResponse(tt.mockedResponseBody)}}) + + client := GitHubClient{ + api: mockedAPI, + } + got, err := client.GetPipeline(tt.path, tt.workflowID) + if err != nil && !tt.wantErr { + ts.Errorf("unexpected error occurred. expected nil, got %s", err) + return + } + + if tt.wantErr { + if err == nil { + ts.Error("expected an error, got nil") + } + return + } + + if tt.want != *got { + ts.Errorf("expected %v, got %v", tt.want, got) + } + + }) + } + +} diff --git a/internal/gateway/gitlab_client.go b/internal/gateway/gitlab_client.go index 0697843..7a86f8d 100644 --- a/internal/gateway/gitlab_client.go +++ b/internal/gateway/gitlab_client.go @@ -1,6 +1,7 @@ package gateway import ( + "errors" "fmt" "strconv" "strings" @@ -60,6 +61,10 @@ func (c *GitLabClient) GetPipeline(id string, pid int) (*Pipeline, error) { return nil, fmt.Errorf("failed to retrieve pipeline: %w", err) } + if pipeline == nil || pipeline.ID == 0 { + return nil, errors.New("no pipelines found") + } + return &Pipeline{ Status: pipeline.Status, ID: pipeline.ID, diff --git a/internal/gateway/gitlab_client_test.go b/internal/gateway/gitlab_client_test.go new file mode 100644 index 0000000..6542ac7 --- /dev/null +++ b/internal/gateway/gitlab_client_test.go @@ -0,0 +1,296 @@ +package gateway + +import ( + "net/http" + "testing" + + "github.com/xanzy/go-gitlab" +) + +func Test_GitLab_GetProjectID(t *testing.T) { + + tests := []struct { + name string + mockedResponseBody string + + path string + + want int + wantErr bool + }{ + {name: "Success", mockedResponseBody: `{"id":12345}`, want: 12345, path: "owner/repo"}, + {name: "Fails due to malformed path", wantErr: true, path: "incorrect path format"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(ts *testing.T) { + mockedAPI, _ := gitlab.NewClient("", gitlab.WithHTTPClient(&http.Client{Transport: &mockRoundTripper{makeJSONResponse(tt.mockedResponseBody)}})) + + client := GitLabClient{ + api: mockedAPI, + } + + got, err := client.GetProjectID(tt.path) + if tt.wantErr { + if err == nil { + ts.Error("expected an error, got nil") + } + return + } + + if tt.want != got { + ts.Errorf("expected %d, got %d", tt.want, got) + } + + }) + } + +} + +func Test_GitLab_GetPipeline(t *testing.T) { + + tests := []struct { + name string + mockedResponseBody string + + path string + pipelineID int + + want Pipeline + wantErr bool + }{ + { + name: "Successfully returns in_progress pipeline", + mockedResponseBody: `{ + "id": 46, + "iid": 11, + "project_id": 1, + "name": "Build pipeline", + "status": "pending", + "ref": "main", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "tag": false, + "yaml_errors": null, + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + "started_at": null, + "finished_at": "2016-08-11T11:32:35.145Z", + "committed_at": null, + "duration": 123, + "queued_duration": 1, + "coverage": "30.0", + "web_url": "https://example.com/gregfurman/pipescope/pipelines/46" + }`, + pipelineID: 46, + path: "https://github.com/gregfurman/pipescope", + want: Pipeline{ + ID: 46, + ProjectID: "1", + CommitSha: "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + Status: "pending", + URL: "https://example.com/gregfurman/pipescope/pipelines/46", + }, + }, + { + name: "Successfully returns completed and failed workflow", + mockedResponseBody: `{ + "id": 46, + "iid": 11, + "project_id": 1, + "name": "Build pipeline", + "status": "failed", + "ref": "main", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "tag": false, + "yaml_errors": null, + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + "started_at": null, + "finished_at": "2016-08-11T11:32:35.145Z", + "committed_at": null, + "duration": 123, + "queued_duration": 1, + "coverage": "30.0", + "web_url": "https://example.com/gregfurman/pipescope/pipelines/46" + }`, + pipelineID: 46, + path: "gregfurman/pipescope", + want: Pipeline{ + ID: 46, + ProjectID: "1", + CommitSha: "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + Status: "failed", + URL: "https://example.com/gregfurman/pipescope/pipelines/46", + }, + }, + { + name: "Fails due to no workflows found", + mockedResponseBody: `{}`, + pipelineID: 46, + path: "https://example.com/gregfurman/pipescope", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(ts *testing.T) { + mockedAPI, _ := gitlab.NewClient("", gitlab.WithHTTPClient(&http.Client{Transport: &mockRoundTripper{makeJSONResponse(tt.mockedResponseBody)}})) + + client := GitLabClient{ + api: mockedAPI, + } + got, err := client.GetPipeline(tt.path, tt.pipelineID) + if err != nil && !tt.wantErr { + ts.Errorf("unexpected error occurred. expected nil, got %s", err) + return + } + + if tt.wantErr { + if err == nil { + ts.Error("expected an error, got nil") + } + return + } + + if tt.want != *got { + ts.Errorf("expected %v, got %v", tt.want, got) + } + + }) + } + +} + +func Test_GitLab_GetPipelineBySha(t *testing.T) { + + tests := []struct { + name string + mockedResponseBody string + + path string + sha string + + want Pipeline + wantErr bool + }{ + { + name: "Successfully returns in_progress pipeline", + mockedResponseBody: `[ + { + "id": 47, + "iid": 12, + "project_id": 1, + "status": "pending", + "source": "push", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "name": "Build pipeline", + "web_url": "https://example.com/gregfurman/pipescope/pipelines/47", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z" + } + ]`, + sha: "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + path: "https://github.com/gregfurman/pipescope", + want: Pipeline{ + ID: 47, + ProjectID: "1", + CommitSha: "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + Status: "pending", + URL: "https://example.com/gregfurman/pipescope/pipelines/47", + }, + }, + { + name: "Successfully returns completed and failed workflow", + mockedResponseBody: `[ + { + "id": 48, + "iid": 13, + "project_id": 1, + "status": "failed", + "source": "web", + "ref": "new-pipeline", + "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", + "name": "Build pipeline", + "web_url": "https://example.com/gregfurman/pipescope/pipelines/48", + "created_at": "2016-08-12T10:06:04.561Z", + "updated_at": "2016-08-12T10:09:56.223Z" + }, + { + "id": 47, + "iid": 12, + "project_id": 1, + "status": "pending", + "source": "push", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "name": "Build pipeline", + "web_url": "https://example.com/gregfurman/pipescope/pipelines/47", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z" + } + ]`, + path: "https://example.com/gregfurman/pipescope/pipelines/48", + sha: "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", + want: Pipeline{ + ID: 48, + ProjectID: "1", + CommitSha: "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", + Status: "failed", + URL: "https://example.com/gregfurman/pipescope/pipelines/48", + }, + }, + { + name: "Fails due to no workflows found", + mockedResponseBody: `[]`, + path: "https://example.com/gregfurman/pipescope", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(ts *testing.T) { + mockedAPI, _ := gitlab.NewClient("", gitlab.WithHTTPClient(&http.Client{Transport: &mockRoundTripper{makeJSONResponse(tt.mockedResponseBody)}})) + + client := GitLabClient{ + api: mockedAPI, + } + got, err := client.GetPipelineBySha(tt.path, tt.sha) + if err != nil && !tt.wantErr { + ts.Errorf("unexpected error occurred. expected nil, got %s", err) + return + } + + if tt.wantErr { + if err == nil { + ts.Error("expected an error, got nil") + } + return + } + + if tt.want != *got { + ts.Errorf("expected %v, got %v", tt.want, got) + } + + }) + } + +} diff --git a/internal/git/client.go b/internal/git/client.go index 91376db..1d634e5 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -6,22 +6,27 @@ import ( "github.com/go-git/go-git/v5" ) -type Client struct { +type Client interface { + GetHead() (string, error) + GetRemoteURL() (string, error) +} + +type ClientImpl struct { repo *git.Repository } -func New(path string) (*Client, error) { +func New(path string) (*ClientImpl, error) { repo, err := git.PlainOpen(path) if err != nil { return nil, fmt.Errorf("failed to create new Git repository: %w", err) } - return &Client{ + return &ClientImpl{ repo: repo, }, nil } -func (c *Client) GetHead() (string, error) { +func (c *ClientImpl) GetHead() (string, error) { head, err := c.repo.Head() if err != nil { return "", fmt.Errorf("failed to get HEAD of repository: %w", err) @@ -30,7 +35,7 @@ func (c *Client) GetHead() (string, error) { return head.Hash().String(), nil } -func (c *Client) GetRemoteURL() (string, error) { +func (c *ClientImpl) GetRemoteURL() (string, error) { remote, err := c.repo.Remote("origin") if err != nil { return "", fmt.Errorf("failed to get remote URL of repository: %w", err)