package auth
import (
"context"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"gitcode-mcp/internal/config"
)
type testSource struct {
env map[string]string
}
func newTestSource() *testSource {
return &testSource{env: map[string]string{}}
}
func (s *testSource) Env(key string) string { return s.env[key] }
func (s *testSource) UserHomeDir() (string, error) { return "/tmp/test-home", nil }
func (s *testSource) UserConfigDir() (string, error) { return "/tmp/test-config", nil }
func (s *testSource) UserCacheDir() (string, error) { return "/tmp/test-cache", nil }
func (s *testSource) ReadFile(path string) ([]byte, error) { return nil, nil }
func TestCredentialResolverEnvTokenPresent(t *testing.T) {
src := newTestSource()
src.env[config.EnvToken] = "secret-token-value"
resolver := NewCredentialResolver(src)
result := resolver.Resolve(context.Background(), config.EffectiveConfig{})
if !result.Present {
t.Fatalf("expected credential present, got Present=%t", result.Present)
}
if result.Source != "env:GITCODE_TOKEN" {
t.Fatalf("expected source env:GITCODE_TOKEN, got %q", result.Source)
}
if result.Token != "secret-token-value" {
t.Fatalf("expected token 'secret-token-value', got %q", result.Token)
}
if result.ErrorClass != "" {
t.Fatalf("expected no error_class, got %q", result.ErrorClass)
}
}
func TestCredentialResolverMockKeyring(t *testing.T) {
src := newTestSource()
src.env[config.EnvToken] = ""
src.env["GITCODE_MCP_TEST_KEYCHAIN_TOKEN"] = "keyring-secret-value"
resolver := NewCredentialResolver(src)
result := resolver.Resolve(context.Background(), config.EffectiveConfig{})
if !result.Present {
t.Fatalf("expected credential present, got Present=%t", result.Present)
}
if result.Source != "mock-keyring" {
t.Fatalf("expected source mock-keyring, got %q", result.Source)
}
if result.Token != "keyring-secret-value" {
t.Fatalf("expected token 'keyring-secret-value', got %q", result.Token)
}
}
func TestCredentialResolverNoCredential(t *testing.T) {
src := newTestSource()
resolver := NewCredentialResolverWithProvider(config.ChainCredentialProvider{
Providers: []config.CredentialProvider{
config.EnvCredentialProvider{Source: src},
},
})
result := resolver.Resolve(context.Background(), config.EffectiveConfig{})
if result.Present {
t.Fatalf("expected credential not present, got Present=%t", result.Present)
}
if result.ErrorClass != "token-missing" {
t.Fatalf("expected error_class token-missing, got %q", result.ErrorClass)
}
if result.Remediation == "" {
t.Fatalf("expected non-empty remediation, got %q", result.Remediation)
}
if !strings.Contains(result.Remediation, "GITCODE_TOKEN") {
t.Fatalf("expected remediation mentioning GITCODE_TOKEN, got %q", result.Remediation)
}
}
func TestCredentialResolverDeterministic(t *testing.T) {
src := newTestSource()
src.env[config.EnvToken] = "deterministic-token"
resolver := NewCredentialResolver(src)
r1 := resolver.Resolve(context.Background(), config.EffectiveConfig{})
r2 := resolver.Resolve(context.Background(), config.EffectiveConfig{})
if r1.Present != r2.Present || r1.Source != r2.Source || r1.Token != r2.Token {
t.Fatalf("resolver not deterministic: r1=%#v r2=%#v", r1, r2)
}
}
func TestCredentialResolverStatusMatchesResolve(t *testing.T) {
src := newTestSource()
src.env[config.EnvToken] = "status-token"
resolver := NewCredentialResolver(src)
statusResult := resolver.Status(context.Background(), config.EffectiveConfig{})
resolveResult := resolver.Resolve(context.Background(), config.EffectiveConfig{})
if statusResult.Present != resolveResult.Present || statusResult.Source != resolveResult.Source {
t.Fatalf("Status() doesn't match Resolve(): status=%#v resolve=%#v", statusResult, resolveResult)
}
}
func TestCredentialResolverEnvOverKeyring(t *testing.T) {
src := newTestSource()
src.env[config.EnvToken] = "env-token"
src.env["GITCODE_MCP_TEST_KEYCHAIN_TOKEN"] = "keyring-token"
resolver := NewCredentialResolver(src)
result := resolver.Resolve(context.Background(), config.EffectiveConfig{})
if !result.Present {
t.Fatalf("expected credential present, got Present=%t", result.Present)
}
if result.Source != "env:GITCODE_TOKEN" {
t.Fatalf("expected source env:GITCODE_TOKEN, got %q (keyring should not take priority)", result.Source)
}
if result.Token != "env-token" {
t.Fatalf("expected token 'env-token', got %q", result.Token)
}
}
type blockingCredentialFixture struct {
calls atomic.Int64
started chan struct{}
release chan struct{}
}
func (p *blockingCredentialFixture) Resolve(ctx context.Context, _ config.EffectiveConfig) (config.SecretString, config.CredentialStatus, error) {
p.calls.Add(1)
close(p.started)
select {
case <-p.release:
return config.NewSecretString("synthetic-private-credential"), config.CredentialStatus{Source: "mock-keyring", Present: true}, nil
case <-ctx.Done():
return config.SecretString{}, config.CredentialStatus{}, ctx.Err()
}
}
func (p *blockingCredentialFixture) Status(ctx context.Context, eff config.EffectiveConfig) config.CredentialStatus {
_, status, _ := p.Resolve(ctx, eff)
return status
}
func TestCredentialResolverConcurrentLazyFirstUseDoesNotWaitOrCacheBusy(t *testing.T) {
p := &blockingCredentialFixture{started: make(chan struct{}), release: make(chan struct{})}
r := NewCredentialResolverWithProvider(p)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan Result, 1)
go func() { done <- r.Resolve(ctx, config.EffectiveConfig{}) }()
select {
case <-p.started:
case <-time.After(time.Second):
t.Fatal("credential fixture did not start")
}
var wg sync.WaitGroup
var wrong atomic.Bool
start := time.Now()
for i := 0; i < 32; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result := r.Status(context.Background(), config.EffectiveConfig{})
if result.Present || result.Token != "" || result.ErrorClass != "credential-resolution-busy" {
wrong.Store(true)
}
}()
}
wg.Wait()
if wrong.Load() || p.calls.Load() != 1 || time.Since(start) > time.Second {
t.Error("concurrent first use waited, amplified credential calls or cached busy state")
}
close(p.release)
select {
case result := <-done:
if !result.Present {
t.Fatal("owning credential resolution failed")
}
case <-time.After(time.Second):
t.Fatal("credential owner did not join")
}
if !r.Status(context.Background(), config.EffectiveConfig{}).Present || p.calls.Load() != 1 {
t.Fatal("completed credential result was not reused")
}
}
func TestCredentialResolverCancelledReadDoesNotResolveOrCacheAbsence(t *testing.T) {
src := newTestSource()
src.env[config.EnvToken] = "synthetic-private-credential"
r := NewCredentialResolverWithProvider(config.EnvCredentialProvider{Source: src})
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := r.Resolve(ctx, config.EffectiveConfig{})
if result.Present || result.ErrorClass != "credential-resolution-cancelled" || r.result != nil {
t.Fatal("cancelled resolution cached absence or ignored context")
}
if !r.Resolve(context.Background(), config.EffectiveConfig{}).Present {
t.Fatal("cancelled first use prevented later explicit resolution")
}
}