mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
@@ -0,0 +1,817 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user