mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
579daaff70
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
818 lines
25 KiB
Go
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)
|
|
})
|
|
}
|
|
})
|
|
}
|