Files
coder/coderd/externalauth/gitprovider/gitprovider_internal_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

151 lines
3.6 KiB
Go

package gitprovider
import (
"net/http"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/coder/quartz"
)
func TestCountDiffLines(t *testing.T) {
t.Parallel()
tests := []struct {
name string
diff string
additions int32
deletions int32
}{
{
name: "Empty",
},
{
name: "OnlyAdditions",
diff: "+a\n+b\n+c\n",
additions: 3,
},
{
name: "OnlyDeletions",
diff: "-a\n-b\n",
deletions: 2,
},
{
name: "MixedWithHeaders",
diff: "--- a/file.txt\n+++ b/file.txt\n@@ -1,2 +1,3 @@\n unchanged\n-old\n+new\n+another\n",
additions: 2,
deletions: 1,
},
{
name: "NoTrailingNewline",
diff: "@@ -1 +1 @@\n-old\n+new",
additions: 1,
deletions: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
additions, deletions := countDiffLines(tt.diff)
assert.Equal(t, tt.additions, additions)
assert.Equal(t, tt.deletions, deletions)
})
}
}
func TestParseRetryAfter(t *testing.T) {
t.Parallel()
clk := quartz.NewMock(t)
clk.Set(time.Date(2026, 5, 25, 12, 0, 0, 0, time.UTC))
t.Run("RetryAfterSeconds", func(t *testing.T) {
t.Parallel()
h := http.Header{}
h.Set("Retry-After", "120")
d := parseRetryAfter(h, "X-Ratelimit-Reset", clk)
assert.Equal(t, 120*time.Second, d)
})
t.Run("GitHubResetHeader", func(t *testing.T) {
t.Parallel()
future := clk.Now().Add(90 * time.Second)
h := http.Header{}
h.Set("X-Ratelimit-Reset", strconv.FormatInt(future.Unix(), 10))
d := parseRetryAfter(h, "X-Ratelimit-Reset", clk)
assert.WithinDuration(t, future, clk.Now().Add(d), time.Second)
})
t.Run("GitLabResetHeader", func(t *testing.T) {
t.Parallel()
future := clk.Now().Add(45 * time.Second)
h := http.Header{}
h.Set("RateLimit-Reset", strconv.FormatInt(future.Unix(), 10))
d := parseRetryAfter(h, "RateLimit-Reset", clk)
assert.WithinDuration(t, future, clk.Now().Add(d), time.Second)
})
t.Run("NoHeaders", func(t *testing.T) {
t.Parallel()
h := http.Header{}
d := parseRetryAfter(h, "X-Ratelimit-Reset", clk)
assert.Equal(t, time.Duration(0), d)
})
t.Run("InvalidValue", func(t *testing.T) {
t.Parallel()
h := http.Header{}
h.Set("Retry-After", "not-a-number")
d := parseRetryAfter(h, "X-Ratelimit-Reset", clk)
assert.Equal(t, time.Duration(0), d)
})
t.Run("RetryAfterTakesPrecedence", func(t *testing.T) {
t.Parallel()
h := http.Header{}
h.Set("Retry-After", "60")
h.Set("X-Ratelimit-Reset", strconv.FormatInt(clk.Now().Add(120*time.Second).Unix(), 10))
d := parseRetryAfter(h, "X-Ratelimit-Reset", clk)
assert.Equal(t, 60*time.Second, d)
})
t.Run("NilClock", func(t *testing.T) {
t.Parallel()
h := http.Header{}
h.Set("Retry-After", "1")
d := parseRetryAfter(h, "X-Ratelimit-Reset", nil)
assert.Equal(t, time.Second, d)
})
}
func TestMapGitLabState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expect PRState
}{
{name: "opened", input: "opened", expect: PRStateOpen},
{name: "Opened_mixed_case", input: "Opened", expect: PRStateOpen},
{name: "merged", input: "merged", expect: PRStateMerged},
{name: "closed", input: "closed", expect: PRStateClosed},
{name: "locked", input: "locked", expect: PRStateClosed},
{name: "unknown_defaults_to_closed", input: "something_else", expect: PRStateClosed},
{name: "empty_defaults_to_closed", input: "", expect: PRStateClosed},
{name: "whitespace_trimmed", input: " opened ", expect: PRStateOpen},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := mapGitLabState(tt.input)
assert.Equal(t, tt.expect, got)
})
}
}