Files
coder/coderd/externalauth/gitprovider/gitlab_integration_test.go
Cian Johnston 579daaff70 feat: add GitLab support to coderd/externalauth/gitprovider
Fixes CODAGT-146

Add GitLab support to the gitprovider package for gitsync/chatd PR
diff flows. This is a squashed stack of 3 PRs:

#25651 - refactor(coderd/externalauth): prepare gitprovider for multi-provider support
- Change gitprovider.New to return (Provider, error)
- Extract shared helpers (parseRetryAfter, checkRateLimitError,
  countDiffLines, escapePathPreserveSlashes) from github.go
- Update all callers (db2sdk, exp_chats, gitsync) for new signature
- Add error logging for provider construction failures
- Thread context through provider resolution

#25652 - feat(coderd/externalauth/gitprovider): add GitLab provider
- Implement full Provider interface: FetchPullRequestStatus,
  FetchPullRequestDiff, FetchBranchDiff, ResolveBranchPullRequest
- Handle nested groups, forks, and self-hosted instances
- Rate limit detection on both library and raw HTTP paths
- URL parsing/building with NormalizePullRequestURL support
- Unit tests covering error paths, URL parsing, state mapping
- Document GitLab configuration and known limitations

#25653 - test(coderd/externalauth/gitprovider): add GitLab VCR integration tests
- FetchPullRequestStatus: 4 fixtures (open, conflicts, merged, closed)
- FetchPullRequestDiff: 4 fixtures
- FetchBranchDiff: 3 fixtures (open, deleted, fork)
- ResolveBranchPullRequest: 3 fixtures
- go-vcr cassettes with sanitized GitLab API responses
2026-05-25 17:41:02 +01:00

818 lines
25 KiB
Go

package gitprovider_test
import (
"net/http"
"os"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
"github.com/coder/coder/v2/coderd/externalauth/gitprovider"
"github.com/coder/coder/v2/testutil"
)
// newGitLabVCR creates a go-vcr recorder for GitLab integration tests.
// In replay mode (default), it serves responses from the cassette file.
// When GITLAB_UPDATE_GOLDEN=true, it records live responses to the cassette.
func newGitLabVCR(t *testing.T, cassetteName string) *recorder.Recorder {
t.Helper()
mode := recorder.ModeReplayOnly
if update, _ := strconv.ParseBool(os.Getenv("GITLAB_UPDATE_GOLDEN")); update {
mode = recorder.ModeRecordOnly
}
rec, err := recorder.New(
"testdata/gitlab_cassettes/"+cassetteName,
recorder.WithMode(mode),
recorder.WithSkipRequestLatency(true),
// Match only on method + URL; the default matcher is too strict
// (compares proto, all headers, etc.) and breaks replay.
// TODO: consider verifying that an Authorization header is present
// during replay to catch auth-wiring regressions.
recorder.WithMatcher(func(r *http.Request, i cassette.Request) bool {
return r.Method == i.Method && r.URL.String() == i.URL
}),
// Strip headers down to an allowlist to reduce cassette noise.
recorder.WithHook(func(i *cassette.Interaction) error {
allowedRequestHeaders := map[string]struct{}{
"Accept": {},
"Content-Type": {},
}
for h := range i.Request.Headers {
if _, ok := allowedRequestHeaders[h]; !ok {
i.Request.Headers[h] = []string{"stripped"}
}
}
allowedResponseHeaders := map[string]struct{}{
"Content-Type": {},
"X-Total": {},
}
for h := range i.Response.Headers {
if _, ok := allowedResponseHeaders[h]; !ok {
i.Response.Headers[h] = []string{"stripped"}
}
}
return nil
}, recorder.AfterCaptureHook),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, rec.Stop())
})
return rec
}
// TestGitLabIntegration exercises every gitprovider.Provider method
// against recorded GitLab API responses (go-vcr cassettes).
//
// To update cassettes from live GitLab:
//
// GITLAB_UPDATE_GOLDEN=true GITLAB_TOKEN=<pat> go test ./coderd/externalauth/gitprovider/ -run TestGitLabIntegration -count=1
//
// Fixtures:
//
// 1. https://gitlab.com/test-group9945421/test-project/-/merge_requests/3
// Simple namespace (single-level group).
// State: open. Same-repo MR, 1 file, mergeable.
//
// 2. https://gitlab.com/test-group9945421/test-project/-/merge_requests/2
// Simple namespace (single-level group).
// State: open. Same-repo MR, 1 file, has conflicts.
//
// 3. https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/1
// Nested group (multi-level namespace: test-group9945421/test-subgroup).
// State: merged. Same-repo MR, 1 file. Source branch deleted after merge.
//
// 4. https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/3
// Nested group. State: closed (not merged). From a fork.
// Source branch "forked" does not exist on the target project.
func TestGitLabIntegration(t *testing.T) {
t.Parallel()
apiURL := "https://gitlab.com"
// Token is only used when recording (GITLAB_UPDATE_GOLDEN=true).
token := os.Getenv("GITLAB_TOKEN")
// URL parsing tests don't need VCR (no API calls).
provider, err := gitprovider.New("gitlab", apiURL, http.DefaultClient)
require.NoError(t, err)
require.NotNil(t, provider, "gitprovider.New returned nil for \"gitlab\"")
// --- URL parsing (no API calls) ---
t.Run("ParseRepositoryOrigin", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
expectOK bool
expectOwner string
expectRepo string
expectNormalized string
}{
{
name: "HTTPS simple",
raw: "https://gitlab.com/test-group9945421/test-project.git",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-project",
},
{
name: "HTTPS no .git",
raw: "https://gitlab.com/test-group9945421/test-project",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-project",
},
{
name: "HTTPS trailing slash",
raw: "https://gitlab.com/test-group9945421/test-project/",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-project",
},
{
name: "SSH",
raw: "git@gitlab.com:test-group9945421/test-project.git",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-project",
},
{
name: "SSH prefix",
raw: "ssh://git@gitlab.com/test-group9945421/test-project.git",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-project",
},
{
name: "Nested group HTTPS",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project.git",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project",
},
{
name: "Nested group HTTPS no .git",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project",
},
{
name: "Nested group SSH",
raw: "git@gitlab.com:test-group9945421/test-subgroup/another-test-project.git",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project",
},
{
name: "Nested group SSH prefix",
raw: "ssh://git@gitlab.com/test-group9945421/test-subgroup/another-test-project.git",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNormalized: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project",
},
{
name: "GitHub does not match",
raw: "https://github.com/coder/coder",
expectOK: false,
},
{
name: "Empty string",
raw: "",
expectOK: false,
},
{
name: "Not a URL",
raw: "not-a-url",
expectOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
owner, repo, normalized, ok := provider.ParseRepositoryOrigin(tt.raw)
assert.Equal(t, tt.expectOK, ok)
if tt.expectOK {
assert.Equal(t, tt.expectOwner, owner)
assert.Equal(t, tt.expectRepo, repo)
assert.Equal(t, tt.expectNormalized, normalized)
}
})
}
})
t.Run("ParsePullRequestURL", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
expectOK bool
expectOwner string
expectRepo string
expectNumber int
}{
{
name: "Simple namespace",
raw: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNumber: 3,
},
{
name: "Nested group",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/1",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNumber: 1,
},
{
name: "Nested group second MR",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/3",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNumber: 3,
},
{
name: "With query string",
raw: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3?tab=diffs",
expectOK: true,
expectOwner: "test-group9945421",
expectRepo: "test-project",
expectNumber: 3,
},
{
name: "With fragment",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/1#note_123",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNumber: 1,
},
{
name: "With path suffix (diffs tab)",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/3/diffs",
expectOK: true,
expectOwner: "test-group9945421/test-subgroup",
expectRepo: "another-test-project",
expectNumber: 3,
},
{
name: "GitHub PR does not match",
raw: "https://github.com/coder/coder/pull/123",
expectOK: false,
},
{
name: "Not a MR URL",
raw: "https://gitlab.com/test-group9945421/test-project/-/issues/1",
expectOK: false,
},
{
name: "Empty string",
raw: "",
expectOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ref, ok := provider.ParsePullRequestURL(tt.raw)
assert.Equal(t, tt.expectOK, ok)
if tt.expectOK {
assert.Equal(t, tt.expectOwner, ref.Owner)
assert.Equal(t, tt.expectRepo, ref.Repo)
assert.Equal(t, tt.expectNumber, ref.Number)
}
})
}
})
t.Run("NormalizePullRequestURL", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
expected string
}{
{
name: "Simple, already normalized",
raw: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3",
expected: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3",
},
{
name: "Simple with query and fragment",
raw: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3?tab=diffs#note_123",
expected: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3",
},
{
name: "Nested group with query",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/1?diff_id=1234",
expected: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/1",
},
{
name: "Nested group with path suffix",
raw: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/3/diffs",
expected: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/3",
},
{
name: "Not a MR URL",
raw: "https://example.com/foo",
expected: "",
},
{
name: "Empty string",
raw: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := provider.NormalizePullRequestURL(tt.raw)
assert.Equal(t, tt.expected, got)
})
}
})
t.Run("BuildBranchURL", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
owner string
repo string
branch string
expected string
}{
{
name: "Simple namespace",
owner: "test-group9945421",
repo: "test-project",
branch: "main",
expected: "https://gitlab.com/test-group9945421/test-project/-/tree/main",
},
{
name: "Nested group",
owner: "test-group9945421/test-subgroup",
repo: "another-test-project",
branch: "main",
expected: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/tree/main",
},
{
name: "Branch with special name",
owner: "test-group9945421/test-subgroup",
repo: "another-test-project",
branch: "johnstcn-main-patch-54711",
expected: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/tree/johnstcn-main-patch-54711",
},
{
name: "Empty owner",
owner: "",
repo: "test-project",
branch: "main",
expected: "",
},
{
name: "Empty repo",
owner: "test-group9945421",
repo: "",
branch: "main",
expected: "",
},
{
name: "Empty branch",
owner: "test-group9945421",
repo: "test-project",
branch: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := provider.BuildBranchURL(tt.owner, tt.repo, tt.branch)
assert.Equal(t, tt.expected, got)
})
}
})
t.Run("BuildRepositoryURL", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
owner string
repo string
expected string
}{
{
name: "Simple namespace",
owner: "test-group9945421",
repo: "test-project",
expected: "https://gitlab.com/test-group9945421/test-project",
},
{
name: "Nested group",
owner: "test-group9945421/test-subgroup",
repo: "another-test-project",
expected: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project",
},
{
name: "Empty owner",
owner: "",
repo: "test-project",
expected: "",
},
{
name: "Empty repo",
owner: "test-group9945421",
repo: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := provider.BuildRepositoryURL(tt.owner, tt.repo)
assert.Equal(t, tt.expected, got)
})
}
})
t.Run("BuildPullRequestURL", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ref gitprovider.PRRef
expected string
}{
{
name: "Simple namespace",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "test-project", Number: 3},
expected: "https://gitlab.com/test-group9945421/test-project/-/merge_requests/3",
},
{
name: "Nested group",
ref: gitprovider.PRRef{Owner: "test-group9945421/test-subgroup", Repo: "another-test-project", Number: 1},
expected: "https://gitlab.com/test-group9945421/test-subgroup/another-test-project/-/merge_requests/1",
},
{
name: "Empty owner",
ref: gitprovider.PRRef{Owner: "", Repo: "test-project", Number: 3},
expected: "",
},
{
name: "Empty repo",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "", Number: 3},
expected: "",
},
{
name: "Zero number",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "test-project", Number: 0},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := provider.BuildPullRequestURL(tt.ref)
assert.Equal(t, tt.expected, got)
})
}
})
// --- API calls (use VCR cassettes) ---
t.Run("FetchPullRequestStatus", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ref gitprovider.PRRef
expectState gitprovider.PRState
expectAuthor string
expectHead string
expectBase string
expectBranch string
expectTitle string
expectDraft bool
expectChanges int32
expectApproved bool
expectReviewerCount int32
expectChangesReq bool
}{
{
name: "open_mergeable",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "test-project", Number: 3},
expectState: gitprovider.PRStateOpen,
expectAuthor: "johnstcn",
expectHead: "da57fca657e02c1fbe131402f927d134a34b257b",
expectBase: "main",
expectBranch: "johnstcn-main-patch-98822",
expectTitle: "Open mergeable",
expectDraft: false,
expectChanges: 1,
expectApproved: true,
expectReviewerCount: 0,
expectChangesReq: false,
},
{
name: "open_with_conflicts",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "test-project", Number: 2},
expectState: gitprovider.PRStateOpen,
expectAuthor: "johnstcn",
expectHead: "642379758fa148ff24cba5f676226a3f8e560d73",
expectBase: "main",
expectBranch: "johnstcn-main-patch-84369",
expectTitle: "Open with conflicts",
expectDraft: false,
expectChanges: 1,
expectApproved: true,
expectReviewerCount: 0,
expectChangesReq: false,
},
{
name: "nested_merged",
ref: gitprovider.PRRef{Owner: "test-group9945421/test-subgroup", Repo: "another-test-project", Number: 1},
expectState: gitprovider.PRStateMerged,
expectAuthor: "johnstcn",
expectHead: "ff919f3dc418e4fbffb6fbded7b4c9ae60a4531b",
expectBase: "main",
expectBranch: "johnstcn-main-patch-54711",
expectTitle: "Nested merged",
expectDraft: false,
expectChanges: 1,
expectApproved: true,
expectReviewerCount: 0,
expectChangesReq: false,
},
{
name: "nested_closed_from_fork",
ref: gitprovider.PRRef{Owner: "test-group9945421/test-subgroup", Repo: "another-test-project", Number: 3},
expectState: gitprovider.PRStateClosed,
expectAuthor: "johnstcn",
expectHead: "6b743c6728fa248e3654657e0e576eafcf472953",
expectBase: "main",
expectBranch: "forked",
expectTitle: "Nested closed from fork",
expectDraft: false,
expectChanges: 1,
expectApproved: true,
expectReviewerCount: 0,
expectChangesReq: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
rec := newGitLabVCR(t, "FetchPullRequestStatus/"+tt.name)
vcrProvider, err := gitprovider.New("gitlab", apiURL, rec.GetDefaultClient())
require.NoError(t, err)
require.NotNil(t, vcrProvider)
status, err := vcrProvider.FetchPullRequestStatus(ctx, token, tt.ref)
require.NoError(t, err)
require.NotNil(t, status)
assert.Equal(t, tt.expectState, status.State)
assert.Equal(t, tt.expectDraft, status.Draft)
assert.Equal(t, tt.ref.Number, status.PRNumber)
assert.False(t, status.FetchedAt.IsZero())
assert.WithinDuration(t, time.Now(), status.FetchedAt, 10*time.Second)
// Fields that are always populated.
assert.NotEmpty(t, status.Title)
assert.NotEmpty(t, status.HeadSHA)
assert.NotEmpty(t, status.HeadBranch)
assert.NotEmpty(t, status.BaseBranch)
assert.NotEmpty(t, status.AuthorLogin)
// Exact assertions for publicly-verifiable fixtures.
if tt.expectAuthor != "" {
assert.Equal(t, tt.expectAuthor, status.AuthorLogin)
}
if tt.expectHead != "" {
assert.Equal(t, tt.expectHead, status.HeadSHA)
}
if tt.expectBase != "" {
assert.Equal(t, tt.expectBase, status.BaseBranch)
}
if tt.expectBranch != "" {
assert.Equal(t, tt.expectBranch, status.HeadBranch)
}
if tt.expectTitle != "" {
assert.Equal(t, tt.expectTitle, status.Title)
}
if tt.expectChanges > 0 {
assert.Equal(t, tt.expectChanges, status.DiffStats.ChangedFiles)
}
// Approval-related fields populated from GitLab approvals endpoint.
assert.Equal(t, tt.expectApproved, status.Approved)
assert.Equal(t, tt.expectReviewerCount, status.ReviewerCount)
assert.Equal(t, tt.expectChangesReq, status.ChangesRequested)
})
}
})
t.Run("FetchPullRequestDiff", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ref gitprovider.PRRef
}{
{
name: "open_mergeable",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "test-project", Number: 3},
},
{
name: "open_with_conflicts",
ref: gitprovider.PRRef{Owner: "test-group9945421", Repo: "test-project", Number: 2},
},
{
name: "nested_merged",
ref: gitprovider.PRRef{Owner: "test-group9945421/test-subgroup", Repo: "another-test-project", Number: 1},
},
{
name: "nested_closed_from_fork",
ref: gitprovider.PRRef{Owner: "test-group9945421/test-subgroup", Repo: "another-test-project", Number: 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
rec := newGitLabVCR(t, "FetchPullRequestDiff/"+tt.name)
vcrProvider, err := gitprovider.New("gitlab", apiURL, rec.GetDefaultClient())
require.NoError(t, err)
require.NotNil(t, vcrProvider)
diff, err := vcrProvider.FetchPullRequestDiff(ctx, token, tt.ref)
require.NoError(t, err)
assert.NotEmpty(t, diff)
assert.Contains(t, diff, "diff --git")
})
}
})
t.Run("ResolveBranchPullRequest", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ref gitprovider.BranchRef
expectNil bool // true if branch is known-deleted or from a fork
}{
{
name: "open_mr_branch",
ref: gitprovider.BranchRef{
Owner: "test-group9945421",
Repo: "test-project",
Branch: "johnstcn-main-patch-98822",
},
expectNil: false,
},
{
name: "nested_branch_deleted_after_merge",
ref: gitprovider.BranchRef{
Owner: "test-group9945421/test-subgroup",
Repo: "another-test-project",
Branch: "johnstcn-main-patch-54711",
},
expectNil: true,
},
{
name: "nested_fork_branch_not_on_target",
ref: gitprovider.BranchRef{
Owner: "test-group9945421/test-subgroup",
Repo: "another-test-project",
Branch: "forked",
},
expectNil: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
rec := newGitLabVCR(t, "ResolveBranchPullRequest/"+tt.name)
vcrProvider, err := gitprovider.New("gitlab", apiURL, rec.GetDefaultClient())
require.NoError(t, err)
require.NotNil(t, vcrProvider)
ref, err := vcrProvider.ResolveBranchPullRequest(ctx, token, tt.ref)
require.NoError(t, err)
if tt.expectNil {
assert.Nil(t, ref)
} else {
require.NotNil(t, ref)
assert.Equal(t, tt.ref.Owner, ref.Owner)
assert.Equal(t, tt.ref.Repo, ref.Repo)
assert.Greater(t, ref.Number, 0)
}
})
}
})
t.Run("FetchBranchDiff", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ref gitprovider.BranchRef
expectErr bool // true if branch no longer exists
}{
{
name: "open_mr_branch",
ref: gitprovider.BranchRef{
Owner: "test-group9945421",
Repo: "test-project",
Branch: "johnstcn-main-patch-98822",
},
},
{
name: "nested_branch_deleted_after_merge",
ref: gitprovider.BranchRef{
Owner: "test-group9945421/test-subgroup",
Repo: "another-test-project",
Branch: "johnstcn-main-patch-54711",
},
// Branch was removed after merge.
expectErr: true,
},
{
name: "nested_fork_branch_not_on_target",
ref: gitprovider.BranchRef{
Owner: "test-group9945421/test-subgroup",
Repo: "another-test-project",
Branch: "forked",
},
// Branch only existed in the fork, not on the target repo.
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
rec := newGitLabVCR(t, "FetchBranchDiff/"+tt.name)
vcrProvider, err := gitprovider.New("gitlab", apiURL, rec.GetDefaultClient())
require.NoError(t, err)
require.NotNil(t, vcrProvider)
diff, err := vcrProvider.FetchBranchDiff(ctx, token, tt.ref)
if tt.expectErr {
// TODO: assert on error content (not just presence) to
// distinguish real API errors from stale-cassette mismatches.
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, diff)
})
}
})
}