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)