Files
kandev/apps/backend/internal/gitcredentials/broker_test.go
T

330 lines
10 KiB
Go

package gitcredentials
import (
"context"
"errors"
"sync"
"testing"
"time"
)
type recordingAuthorizer struct {
mu sync.Mutex
calls int
last Scope
err error
}
func (a *recordingAuthorizer) AuthorizeGitCredential(_ context.Context, scope Scope) error {
a.mu.Lock()
defer a.mu.Unlock()
a.calls++
a.last = scope
return a.err
}
type rotatingProvider struct {
mu sync.Mutex
binding string
username string
secret string
calls int
err error
disabled bool
started chan struct{}
release chan struct{}
}
func (p *rotatingProvider) Supports(providerID string) bool {
p.mu.Lock()
defer p.mu.Unlock()
return providerID == "bitbucket" && !p.disabled
}
func (p *rotatingProvider) Binding(context.Context, Scope) (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
return p.binding, p.err
}
func (p *rotatingProvider) Resolve(ctx context.Context, _ Scope) (Credential, error) {
if p.started != nil {
select {
case p.started <- struct{}{}:
case <-ctx.Done():
return Credential{}, ctx.Err()
}
select {
case <-p.release:
case <-ctx.Done():
return Credential{}, ctx.Err()
}
}
p.mu.Lock()
defer p.mu.Unlock()
p.calls++
if p.err != nil {
return Credential{}, p.err
}
return Credential{Username: p.username, Password: p.secret}, nil
}
func testScope() Scope {
return Scope{
ProviderID: "bitbucket", WorkspaceID: "workspace-1", TaskID: "task-1", SessionID: "session-1",
RepositoryID: "repository-1", Host: "bitbucket.example", Path: "/scm/ENG/widgets.git",
}
}
func testRedemption(lease string) Redemption {
return Redemption{
Lease: lease, TaskID: "task-1", SessionID: "session-1", RepositoryID: "repository-1",
Host: "bitbucket.example", Path: "/scm/ENG/widgets.git",
}
}
func TestBrokerIssuesExactScopedLeaseAndReresolves(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "x-token-auth", secret: "first"}
authorizer := &recordingAuthorizer{}
broker := NewBroker(provider, authorizer)
lease, err := broker.Issue(t.Context(), testScope())
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
credential, err := broker.Redeem(t.Context(), testRedemption(lease.Token))
if err != nil {
t.Fatalf("Redeem() error = %v", err)
}
if credential.Username != "x-token-auth" || credential.Password != "first" {
t.Fatalf("credential = %#v", credential)
}
provider.mu.Lock()
provider.secret = "rotated"
provider.mu.Unlock()
credential, err = broker.Redeem(t.Context(), testRedemption(lease.Token))
if err != nil {
t.Fatalf("second Redeem() error = %v", err)
}
if credential.Password != "rotated" || provider.calls != 2 {
t.Fatalf("second credential = %#v, provider calls = %d", credential, provider.calls)
}
if authorizer.calls != 3 {
t.Fatalf("authorizer calls = %d, want issue + two redemptions", authorizer.calls)
}
}
func TestBrokerRejectsExactScopeMismatch(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
lease, err := broker.Issue(t.Context(), testScope())
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
for name, mismatch := range map[string]func(*Redemption){
"task": func(request *Redemption) { request.TaskID = "other-task" },
"session": func(request *Redemption) { request.SessionID = "other-session" },
"repository": func(request *Redemption) { request.RepositoryID = "other-repository" },
"host": func(request *Redemption) { request.Host = "other.example" },
"path": func(request *Redemption) { request.Path = "/scm/ENG/other.git" },
} {
t.Run(name, func(t *testing.T) {
request := testRedemption(lease.Token)
mismatch(&request)
if _, err := broker.Redeem(t.Context(), request); !errors.Is(err, ErrScopeDenied) {
t.Fatalf("%s mismatch error = %v, want ErrScopeDenied", name, err)
}
})
}
}
func TestBrokerRejectsRepositoryPathCaseMismatch(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
scope := testScope()
scope.Path = "/Team/Repo.git"
lease, err := broker.Issue(t.Context(), scope)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
request := testRedemption(lease.Token)
request.Path = "/team/repo.git"
if _, err := broker.Redeem(t.Context(), request); !errors.Is(err, ErrScopeDenied) {
t.Fatalf("Redeem() error = %v, want ErrScopeDenied", err)
}
}
func TestBrokerRejectsRepositoryGitSuffixMismatch(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
lease, err := broker.Issue(t.Context(), testScope())
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
request := testRedemption(lease.Token)
request.Path = "/scm/ENG/widgets"
if _, err := broker.Redeem(t.Context(), request); !errors.Is(err, ErrScopeDenied) {
t.Fatalf("Redeem() error = %v, want ErrScopeDenied", err)
}
}
func TestBrokerExpiresAndRevokesLease(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
broker.now = func() time.Time { return now }
scope := testScope()
scope.TTL = time.Minute
lease, err := broker.Issue(t.Context(), scope)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
now = now.Add(2 * time.Minute)
if _, err := broker.Redeem(t.Context(), testRedemption(lease.Token)); !errors.Is(err, ErrLeaseExpired) {
t.Fatalf("expired redemption error = %v, want ErrLeaseExpired", err)
}
now = now.Add(-2 * time.Minute)
lease, err = broker.Issue(t.Context(), scope)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
broker.RevokeSession(scope.SessionID)
if _, err := broker.Redeem(t.Context(), testRedemption(lease.Token)); !errors.Is(err, ErrLeaseInvalid) {
t.Fatalf("revoked redemption error = %v, want ErrLeaseInvalid", err)
}
}
func TestBrokerClampsRequestedTTLToDefaultMaximum(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
broker.now = func() time.Time { return now }
scope := testScope()
scope.TTL = defaultLeaseTTL + time.Hour
lease, err := broker.Issue(t.Context(), scope)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
if want := now.Add(defaultLeaseTTL); !lease.ExpiresAt.Equal(want) {
t.Fatalf("ExpiresAt = %s, want clamped maximum %s", lease.ExpiresAt, want)
}
}
func TestBrokerActiveLeaseCountSweepsExpiredRecords(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
broker.now = func() time.Time { return now }
scope := testScope()
scope.TTL = time.Minute
if _, err := broker.Issue(t.Context(), scope); err != nil {
t.Fatalf("Issue() error = %v", err)
}
now = now.Add(2 * time.Minute)
if count := broker.ActiveLeaseCount(); count != 0 {
t.Fatalf("ActiveLeaseCount() = %d, want expired record swept", count)
}
broker.mu.Lock()
retained := len(broker.leases)
broker.mu.Unlock()
if retained != 0 {
t.Fatalf("retained lease records = %d, want 0 after sweep", retained)
}
}
func TestBrokerRenewsLeaseOnlyAfterSuccessfulRedemption(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
issuedAt := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
now := issuedAt
broker.now = func() time.Time { return now }
scope := testScope()
scope.TTL = time.Minute
lease, err := broker.Issue(t.Context(), scope)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
now = issuedAt.Add(30 * time.Second)
if _, err := broker.Redeem(t.Context(), testRedemption(lease.Token)); err != nil {
t.Fatalf("Redeem() error = %v", err)
}
now = issuedAt.Add(75 * time.Second)
if _, err := broker.Redeem(t.Context(), testRedemption(lease.Token)); err != nil {
t.Fatalf("Redeem() after original expiry error = %v", err)
}
}
func TestBrokerConcurrentRevocationNeverReturnsCredential(t *testing.T) {
provider := &rotatingProvider{
binding: "generation-1", username: "user", secret: "secret", started: make(chan struct{}), release: make(chan struct{}),
}
broker := NewBroker(provider, &recordingAuthorizer{})
lease, err := broker.Issue(t.Context(), testScope())
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
result := make(chan struct {
credential Credential
err error
}, 1)
go func() {
credential, redeemErr := broker.Redeem(context.Background(), testRedemption(lease.Token))
result <- struct {
credential Credential
err error
}{credential, redeemErr}
}()
select {
case <-provider.started:
case <-time.After(5 * time.Second):
t.Fatal("Redeem() did not reach provider resolution")
}
broker.RevokeSession("session-1")
close(provider.release)
var got struct {
credential Credential
err error
}
select {
case got = <-result:
case <-time.After(5 * time.Second):
t.Fatal("Redeem() did not return after provider release")
}
if got.credential.Password != "" || !errors.Is(got.err, ErrLeaseRevoked) {
t.Fatalf("Redeem() = %#v, %v; want no credential and ErrLeaseRevoked", got.credential, got.err)
}
}
func TestCompositeResolverRevokesLeaseWhenProviderDisables(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
resolver := NewCompositeResolver(provider)
broker := NewBroker(resolver, &recordingAuthorizer{})
lease, err := broker.Issue(t.Context(), testScope())
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
provider.mu.Lock()
provider.disabled = true
provider.mu.Unlock()
if _, err := broker.Redeem(t.Context(), testRedemption(lease.Token)); !errors.Is(err, ErrLeaseRevoked) {
t.Fatalf("disabled provider redemption error = %v, want ErrLeaseRevoked", err)
}
}
func TestBrokerRevokesProviderLeases(t *testing.T) {
provider := &rotatingProvider{binding: "generation-1", username: "user", secret: "secret"}
broker := NewBroker(provider, &recordingAuthorizer{})
lease, err := broker.Issue(t.Context(), testScope())
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
broker.RevokeProvider("bitbucket")
if _, err := broker.Redeem(t.Context(), testRedemption(lease.Token)); !errors.Is(err, ErrLeaseInvalid) {
t.Fatalf("provider-revoked redemption error = %v, want ErrLeaseInvalid", err)
}
}