Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/api/pullrequest_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ func TestPullRequestHandlerErrResponse(t *testing.T) {
assert.Contains(t, errResp.Message, "unauthorized")
})

t.Run("not found error returns 404", func(t *testing.T) {
err := fmt.Errorf("project deleted-group/deleted-repo: %w", gferrors.ErrNotFound)

resp := handler.errResponse(err)

errResp, ok := resp.(ListPullRequests404JSONResponse)
require.True(t, ok, "expected ListPullRequests404JSONResponse")
assert.Equal(t, fmt.Sprintf("%d", http.StatusNotFound), errResp.Code)
assert.Contains(t, errResp.Message, "not found")
})

t.Run("generic error returns 500", func(t *testing.T) {
resp := handler.errResponse(errors.New("something went wrong"))

Expand Down
31 changes: 30 additions & 1 deletion internal/services/bitbucket/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bitbucket
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
Expand All @@ -27,6 +28,18 @@ import (
// would require changes across all Bitbucket methods and the underlying library.
const defaultBitbucketAPIURL = "https://api.bitbucket.org/2.0"

// isBitbucketNotFound checks whether the error from the go-bitbucket library
// indicates a 404 response. The library returns *UnexpectedResponseStatusError
// with Status set to the HTTP status text (e.g. "404 Not Found").
func isBitbucketNotFound(err error) bool {
var statusErr *bitbucket.UnexpectedResponseStatusError
if errors.As(err, &statusErr) {
return strings.HasPrefix(statusErr.Status, "404")
}

return false
}

type BitbucketService struct {
httpClient *resty.Client
}
Expand Down Expand Up @@ -56,7 +69,7 @@ func (b *BitbucketService) GetRepository(

repository, err := client.Repositories.Repository.Get(repoOptions)
if err != nil {
if err.Error() == "404 Not Found" {
if isBitbucketNotFound(err) {
return nil, fmt.Errorf("repository %s/%s %w", owner, repo, gferrors.ErrNotFound)
}

Expand Down Expand Up @@ -85,6 +98,10 @@ func (b *BitbucketService) ListRepositories(

repositories, err := client.Repositories.ListForAccount(repoOptions)
if err != nil {
if isBitbucketNotFound(err) {
return nil, fmt.Errorf("account %s: %w", account, gferrors.ErrNotFound)
}

return nil, fmt.Errorf("failed to list repositories for account %s: %w", account, err)
}

Expand Down Expand Up @@ -157,6 +174,10 @@ func (b *BitbucketService) ListBranches(

for b, err := range scanBranches {
if err != nil {
if isBitbucketNotFound(err) {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, gferrors.ErrNotFound)
}

return nil, fmt.Errorf("failed to list branches: %w", err)
}

Expand Down Expand Up @@ -266,6 +287,14 @@ func (b *BitbucketService) ListPullRequests(
return nil, fmt.Errorf("failed to list pull requests for %s/%s: %w", owner, repo, err)
}

if resp.StatusCode() == http.StatusNotFound {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, gferrors.ErrNotFound)
}

if resp.StatusCode() == http.StatusUnauthorized || resp.StatusCode() == http.StatusForbidden {
return nil, fmt.Errorf("invalid credentials: %w", gferrors.ErrUnauthorized)
}

if resp.IsError() {
return nil, fmt.Errorf("failed to list pull requests for %s/%s: status %d, body: %s",
owner, repo, resp.StatusCode(), resp.String())
Expand Down
70 changes: 69 additions & 1 deletion internal/services/bitbucket/bitbucket_pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

gferrors "github.com/KubeRocketCI/gitfusion/internal/errors"
"github.com/KubeRocketCI/gitfusion/internal/models"
"github.com/KubeRocketCI/gitfusion/internal/services/krci"
)
Expand Down Expand Up @@ -276,6 +278,7 @@ func TestBitbucketServiceListPullRequests(t *testing.T) {
statusCode int
wantErr bool
wantErrContain string
wantSentinel error
wantCount int
wantState models.PullRequestState
validateQuery func(t *testing.T, r *http.Request)
Expand Down Expand Up @@ -378,7 +381,8 @@ func TestBitbucketServiceListPullRequests(t *testing.T) {
responseBody: bitbucketPRResponse{},
statusCode: http.StatusUnauthorized,
wantErr: true,
wantErrContain: "status 401",
wantErrContain: "unauthorized",
wantSentinel: gferrors.ErrUnauthorized,
},
}

Expand Down Expand Up @@ -424,6 +428,10 @@ func TestBitbucketServiceListPullRequests(t *testing.T) {
assert.Contains(t, err.Error(), tt.wantErrContain)
}

if tt.wantSentinel != nil {
assert.True(t, errors.Is(err, tt.wantSentinel), "error should wrap %v", tt.wantSentinel)
}

return
}

Expand Down Expand Up @@ -768,3 +776,63 @@ func TestBitbucketServiceListPullRequestsSupersededState(t *testing.T) {
assert.Equal(t, models.PullRequestStateClosed, result.Data[0].State)
assert.Equal(t, models.PullRequestStateClosed, result.Data[1].State)
}

func TestBitbucketServiceListPullRequestsNotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"type":"error","error":{"message":"Repository not found"}}`))
}))
defer server.Close()

svc := &BitbucketService{
httpClient: resty.New().SetTransport(&redirectTransport{
target: server.URL,
wrapped: http.DefaultTransport,
}),
}
token := testBitbucketToken()

result, err := svc.ListPullRequests(
context.Background(),
"deleted-owner",
"deleted-repo",
krci.GitServerSettings{Token: token},
models.PullRequestListOptions{State: "open", Page: 1, PerPage: 20},
)

assert.Error(t, err)
assert.Nil(t, result)
assert.True(t, errors.Is(err, gferrors.ErrNotFound), "error should wrap gferrors.ErrNotFound")
assert.Contains(t, err.Error(), "not found")
}

func TestBitbucketServiceListPullRequestsUnauthorized(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"type":"error","error":{"message":"Unauthorized"}}`))
}))
defer server.Close()

svc := &BitbucketService{
httpClient: resty.New().SetTransport(&redirectTransport{
target: server.URL,
wrapped: http.DefaultTransport,
}),
}
token := testBitbucketToken()

result, err := svc.ListPullRequests(
context.Background(),
"owner",
"repo",
krci.GitServerSettings{Token: token},
models.PullRequestListOptions{State: "open", Page: 1, PerPage: 20},
)

assert.Error(t, err)
assert.Nil(t, result)
assert.True(t, errors.Is(err, gferrors.ErrUnauthorized), "error should wrap gferrors.ErrUnauthorized")
assert.Contains(t, err.Error(), "unauthorized")
}
44 changes: 34 additions & 10 deletions internal/services/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ import (
"github.com/KubeRocketCI/gitfusion/pkg/xiter"
)

// classifyGitHubError maps a GitHub API error to a domain sentinel error.
// Returns nil if the error is not a recognized GitHub error response.
func classifyGitHubError(err error) error {
ghErr := &github.ErrorResponse{}
if !errors.As(err, &ghErr) {
return nil
}

switch ghErr.Response.StatusCode {
case http.StatusNotFound:
return gferrors.ErrNotFound
case http.StatusUnauthorized:
return gferrors.ErrUnauthorized
default:
return nil
}
}

const (
stateOpen = "open"
stateClosed = "closed"
Expand All @@ -42,11 +60,8 @@ func (g *GitHubProvider) GetRepository(

repository, _, err := client.Repositories.Get(ctx, owner, repo)
if err != nil {
ghErr := &github.ErrorResponse{}
if errors.As(err, &ghErr) {
if ghErr.Response.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, gferrors.ErrNotFound)
}
if sentinel := classifyGitHubError(err); sentinel != nil {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, sentinel)
}

return nil, fmt.Errorf("failed to get repository %s/%s: %w", owner, repo, err)
Expand All @@ -72,11 +87,8 @@ func (g *GitHubProvider) ListRepositories(

for repo, err := range it {
if err != nil {
ghErr := &github.ErrorResponse{}
if errors.As(err, &ghErr) {
if ghErr.Response.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("organization or user %s: %w", owner, gferrors.ErrNotFound)
}
if sentinel := classifyGitHubError(err); sentinel != nil {
return nil, fmt.Errorf("organization or user %s: %w", owner, sentinel)
}

return nil, fmt.Errorf("failed to list repositories for org %s: %w", owner, err)
Expand Down Expand Up @@ -291,6 +303,10 @@ func (g *GitHubProvider) ListBranches(

for b, err := range it {
if err != nil {
if sentinel := classifyGitHubError(err); sentinel != nil {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, sentinel)
}

return nil, fmt.Errorf("failed to list branches: %w", err)
}

Expand Down Expand Up @@ -339,6 +355,10 @@ func (g *GitHubProvider) listPullRequestsDirect(
},
})
if err != nil {
if sentinel := classifyGitHubError(err); sentinel != nil {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, sentinel)
}

return nil, fmt.Errorf("failed to list pull requests for %s/%s: %w", owner, repo, err)
}

Expand Down Expand Up @@ -415,6 +435,10 @@ func (g *GitHubProvider) listPullRequestsWithPostFilter(
pagesQueried++

if err != nil {
if sentinel := classifyGitHubError(err); sentinel != nil {
return nil, fmt.Errorf("repository %s/%s: %w", owner, repo, sentinel)
}

return nil, fmt.Errorf("failed to list pull requests for %s/%s: %w", owner, repo, err)
}

Expand Down
14 changes: 2 additions & 12 deletions internal/services/github/github_pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ package github

import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"time"

"github.com/google/go-github/v72/github"

gferrors "github.com/KubeRocketCI/gitfusion/internal/errors"
"github.com/KubeRocketCI/gitfusion/internal/models"
"github.com/KubeRocketCI/gitfusion/internal/services/common"
"github.com/KubeRocketCI/gitfusion/internal/services/krci"
Expand Down Expand Up @@ -56,15 +53,8 @@ func (g *GitHubProvider) ListPipelines(

workflowRuns, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, ghOpts)
if err != nil {
ghErr := &github.ErrorResponse{}
if errors.As(err, &ghErr) {
if ghErr.Response.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("project %s: %w", project, gferrors.ErrNotFound)
}

if ghErr.Response.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("invalid credentials: %w", gferrors.ErrUnauthorized)
}
if sentinel := classifyGitHubError(err); sentinel != nil {
return nil, fmt.Errorf("project %s: %w", project, sentinel)
}

return nil, fmt.Errorf("failed to list pipelines for %s: %w", project, err)
Expand Down
Loading
Loading