571 lines
21 KiB
Go
571 lines
21 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type fakeConnectionReader struct {
|
|
workspaces map[string]*WorkspaceConnection
|
|
users map[string]*UserConnection
|
|
}
|
|
|
|
func (f *fakeConnectionReader) GetWorkspaceConnection(_ context.Context, workspaceID string) (*WorkspaceConnection, error) {
|
|
return f.workspaces[workspaceID], nil
|
|
}
|
|
|
|
func (f *fakeConnectionReader) GetUserConnection(_ context.Context, workspaceID, userID string) (*UserConnection, error) {
|
|
return f.users[workspaceID+":"+userID], nil
|
|
}
|
|
|
|
type fakeAuthSecrets map[string]string
|
|
|
|
type repoScopedInstallationProvider struct {
|
|
clients map[string]Client
|
|
calls int
|
|
}
|
|
|
|
type registrationInstallationProvider struct {
|
|
calls map[string]int
|
|
}
|
|
|
|
func (p *registrationInstallationProvider) ResolveInstallation(
|
|
_ context.Context,
|
|
connection *WorkspaceConnection,
|
|
_ ResolveCredentialRequest,
|
|
) (*ResolvedCredential, error) {
|
|
p.calls[connection.AppRegistrationID]++
|
|
return &ResolvedCredential{
|
|
Client: NewMockClient(),
|
|
Principal: AuthPrincipal{
|
|
Kind: AuthPrincipalApp, Source: ConnectionSourceGitHubAppInstallation,
|
|
AppRegistrationID: connection.AppRegistrationID,
|
|
},
|
|
AppRegistrationID: connection.AppRegistrationID,
|
|
AppCredentialGeneration: 7,
|
|
}, nil
|
|
}
|
|
|
|
type expiringInstallationProvider struct {
|
|
now func() time.Time
|
|
lifetime time.Duration
|
|
calls int
|
|
}
|
|
|
|
type synchronizedConnectionReader struct {
|
|
mu sync.Mutex
|
|
connection WorkspaceConnection
|
|
}
|
|
|
|
func (r *synchronizedConnectionReader) GetWorkspaceConnection(
|
|
_ context.Context,
|
|
_ string,
|
|
) (*WorkspaceConnection, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
connection := r.connection
|
|
if r.connection.InstallationID != nil {
|
|
installationID := *r.connection.InstallationID
|
|
connection.InstallationID = &installationID
|
|
}
|
|
return &connection, nil
|
|
}
|
|
|
|
func (r *synchronizedConnectionReader) GetUserConnection(
|
|
context.Context,
|
|
string,
|
|
string,
|
|
) (*UserConnection, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *synchronizedConnectionReader) replace(connection WorkspaceConnection) {
|
|
r.mu.Lock()
|
|
r.connection = connection
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
type generationBarrierProvider struct {
|
|
oldStarted chan struct{}
|
|
releaseOld chan struct{}
|
|
oldClient Client
|
|
newClient Client
|
|
startOnce sync.Once
|
|
mu sync.Mutex
|
|
calls map[int64]int
|
|
}
|
|
|
|
func (p *generationBarrierProvider) ResolveInstallation(
|
|
ctx context.Context,
|
|
connection *WorkspaceConnection,
|
|
_ ResolveCredentialRequest,
|
|
) (*ResolvedCredential, error) {
|
|
p.mu.Lock()
|
|
p.calls[connection.CredentialGeneration]++
|
|
p.mu.Unlock()
|
|
client := p.newClient
|
|
if connection.CredentialGeneration == 1 {
|
|
client = p.oldClient
|
|
p.startOnce.Do(func() { close(p.oldStarted) })
|
|
select {
|
|
case <-p.releaseOld:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
return &ResolvedCredential{
|
|
Client: client,
|
|
Principal: AuthPrincipal{Kind: AuthPrincipalApp, Source: ConnectionSourceGitHubAppInstallation},
|
|
}, nil
|
|
}
|
|
|
|
func (p *expiringInstallationProvider) ResolveInstallation(
|
|
_ context.Context,
|
|
_ *WorkspaceConnection,
|
|
_ ResolveCredentialRequest,
|
|
) (*ResolvedCredential, error) {
|
|
p.calls++
|
|
return &ResolvedCredential{
|
|
Client: NewMockClient(),
|
|
Principal: AuthPrincipal{Kind: AuthPrincipalApp},
|
|
ExpiresAt: p.now().Add(p.lifetime),
|
|
}, nil
|
|
}
|
|
|
|
func (p *repoScopedInstallationProvider) ResolveInstallation(
|
|
_ context.Context,
|
|
_ *WorkspaceConnection,
|
|
req ResolveCredentialRequest,
|
|
) (*ResolvedCredential, error) {
|
|
p.calls++
|
|
client := NewMockClient()
|
|
p.clients[req.RepoOwner+"/"+req.RepoName] = client
|
|
return &ResolvedCredential{
|
|
Client: client,
|
|
Principal: AuthPrincipal{Kind: AuthPrincipalApp, Source: ConnectionSourceGitHubAppInstallation},
|
|
}, nil
|
|
}
|
|
|
|
func (f fakeAuthSecrets) Reveal(_ context.Context, id string) (string, error) {
|
|
value, ok := f[id]
|
|
if !ok {
|
|
return "", errors.New("missing secret")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func TestCredentialResolverIsolatesWorkspacePATs(t *testing.T) {
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"personal": {WorkspaceID: "personal", Source: ConnectionSourcePAT, Login: "alice", Status: ConnectionStatusActive, CredentialGeneration: 1},
|
|
"work": {WorkspaceID: "work", Source: ConnectionSourcePAT, Login: "alice-work", Status: ConnectionStatusActive, CredentialGeneration: 1},
|
|
}}
|
|
resolver := NewCredentialResolver(connections, fakeAuthSecrets{
|
|
WorkspacePATSecretKey("personal"): "pat-personal",
|
|
WorkspacePATSecretKey("work"): "pat-work",
|
|
})
|
|
|
|
personal, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "personal", Purpose: CredentialPurposeAutomation})
|
|
if err != nil {
|
|
t.Fatalf("resolve personal: %v", err)
|
|
}
|
|
work, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "work", Purpose: CredentialPurposeAutomation})
|
|
if err != nil {
|
|
t.Fatalf("resolve work: %v", err)
|
|
}
|
|
if personal.Client == work.Client || personal.RateTracker == work.RateTracker {
|
|
t.Fatal("workspace credentials share client or rate tracker")
|
|
}
|
|
if personal.Principal.Login != "alice" || work.Principal.Login != "alice-work" {
|
|
t.Fatalf("principals = %+v / %+v", personal.Principal, work.Principal)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverNeverFallsBackForDisconnectedWorkspace(t *testing.T) {
|
|
resolver := NewCredentialResolver(&fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{}}, nil)
|
|
resolver.SetLegacyFactory(func(context.Context) (Client, string, error) {
|
|
t.Fatal("legacy factory called for disconnected workspace")
|
|
return nil, "", nil
|
|
})
|
|
_, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "new", Purpose: CredentialPurposeAutomation})
|
|
if !errors.Is(err, ErrGitHubNotConfigured) {
|
|
t.Fatalf("error = %v, want ErrGitHubNotConfigured", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthResolverKeysAppCredentialsByRegistration(t *testing.T) {
|
|
installationID := int64(42)
|
|
connection := &WorkspaceConnection{
|
|
WorkspaceID: "work", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, AppRegistrationID: "registration-a",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
}
|
|
reader := &synchronizedConnectionReader{connection: *connection}
|
|
provider := ®istrationInstallationProvider{calls: make(map[string]int)}
|
|
resolver := NewCredentialResolver(reader, nil)
|
|
resolver.SetInstallationProvider(provider)
|
|
|
|
first, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "work", Purpose: CredentialPurposeAutomation,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection.AppRegistrationID = "registration-b"
|
|
reader.replace(*connection)
|
|
second, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "work", Purpose: CredentialPurposeAutomation,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Principal.AppRegistrationID != "registration-a" ||
|
|
second.Principal.AppRegistrationID != "registration-b" {
|
|
t.Fatalf("principals = %+v / %+v", first.Principal, second.Principal)
|
|
}
|
|
if provider.calls["registration-a"] != 1 || provider.calls["registration-b"] != 1 {
|
|
t.Fatalf("provider calls = %#v", provider.calls)
|
|
}
|
|
}
|
|
|
|
func TestAuthResolverAppConnectionRequiresRegistrationID(t *testing.T) {
|
|
installationID := int64(42)
|
|
resolver := NewCredentialResolver(&fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"work": {
|
|
WorkspaceID: "work", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, Status: ConnectionStatusActive,
|
|
CredentialGeneration: 1,
|
|
},
|
|
}}, nil)
|
|
resolver.SetInstallationProvider(®istrationInstallationProvider{calls: make(map[string]int)})
|
|
_, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "work", Purpose: CredentialPurposeAutomation,
|
|
})
|
|
if !errors.Is(err, ErrGitHubNotConfigured) {
|
|
t.Fatalf("error = %v, want ErrGitHubNotConfigured", err)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverUsesLegacyOnlyForMigratedSource(t *testing.T) {
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"old": {WorkspaceID: "old", Source: ConnectionSourceLegacyShared, Status: ConnectionStatusActive, CredentialGeneration: 1},
|
|
}}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
legacy := NewMockClient()
|
|
legacy.SetUser("legacy-user")
|
|
resolver.SetLegacyFactory(func(context.Context) (Client, string, error) { return legacy, AuthMethodPAT, nil })
|
|
|
|
resolved, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "old", Purpose: CredentialPurposeAutomation})
|
|
if err != nil {
|
|
t.Fatalf("resolve legacy: %v", err)
|
|
}
|
|
if resolved.Principal.Source != ConnectionSourceLegacyShared || resolved.Principal.Login != "legacy-user" {
|
|
t.Fatalf("principal = %+v", resolved.Principal)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverNamedCLIUsesSelectedLogin(t *testing.T) {
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"work": {WorkspaceID: "work", Source: ConnectionSourceGHCLI, GitHubHost: "github.com", Login: "work-user", Status: ConnectionStatusActive, CredentialGeneration: 2},
|
|
}}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
var gotHost, gotLogin string
|
|
resolver.ghToken = func(_ context.Context, host, login string) (string, error) {
|
|
gotHost, gotLogin = host, login
|
|
return "cli-token", nil
|
|
}
|
|
|
|
resolved, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "work", Purpose: CredentialPurposeAutomation})
|
|
if err != nil {
|
|
t.Fatalf("resolve CLI: %v", err)
|
|
}
|
|
if gotHost != "github.com" || gotLogin != "work-user" {
|
|
t.Fatalf("selected account = %s@%s", gotLogin, gotHost)
|
|
}
|
|
if resolved.Principal.Source != ConnectionSourceGHCLI {
|
|
t.Fatalf("principal = %+v", resolved.Principal)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverNamedCLIRefreshesAfterFiveMinutes(t *testing.T) {
|
|
now := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC)
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"work": {WorkspaceID: "work", Source: ConnectionSourceGHCLI, GitHubHost: "github.com", Login: "work-user", Status: ConnectionStatusActive, CredentialGeneration: 2},
|
|
}}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
resolver.now = func() time.Time { return now }
|
|
calls := 0
|
|
resolver.ghToken = func(_ context.Context, _, _ string) (string, error) {
|
|
calls++
|
|
return fmt.Sprintf("cli-token-%d", calls), nil
|
|
}
|
|
request := ResolveCredentialRequest{WorkspaceID: "work", Purpose: CredentialPurposeAutomation}
|
|
|
|
first, err := resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = now.Add(4*time.Minute + 59*time.Second)
|
|
stillCached, err := resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = now.Add(time.Second)
|
|
refreshed, err := resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls != 2 || first.credential != stillCached.credential || refreshed.credential == first.credential {
|
|
t.Fatalf("calls=%d credentials=%q/%q/%q", calls, first.credential, stillCached.credential, refreshed.credential)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverAppRequiresPersonalIdentityForMyGitHub(t *testing.T) {
|
|
installationID := int64(42)
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"company": {WorkspaceID: "company", Source: ConnectionSourceGitHubAppInstallation, InstallationID: &installationID, AppRegistrationID: "registration-test", Status: ConnectionStatusActive, CredentialGeneration: 1},
|
|
}}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
_, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", UserID: "default-user", Purpose: CredentialPurposePersonalRead,
|
|
})
|
|
if !errors.Is(err, ErrGitHubPersonalRequired) {
|
|
t.Fatalf("error = %v, want ErrGitHubPersonalRequired", err)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverInvalidPersonalFallsBackToAppOnlyForManualWrites(t *testing.T) {
|
|
installationID := int64(42)
|
|
connections := &fakeConnectionReader{
|
|
workspaces: map[string]*WorkspaceConnection{
|
|
"company": {
|
|
WorkspaceID: "company", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, AppRegistrationID: "registration-test",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
},
|
|
},
|
|
users: map[string]*UserConnection{
|
|
"company:default-user": {
|
|
WorkspaceID: "company", UserID: "default-user", Login: "octocat",
|
|
Status: ConnectionStatusRevoked, CredentialGeneration: 2,
|
|
},
|
|
},
|
|
}
|
|
provider := &repoScopedInstallationProvider{clients: make(map[string]Client)}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
resolver.SetInstallationProvider(provider)
|
|
resolver.SetUserProvider(fixedUserCredentialProvider{client: NewMockClient()})
|
|
|
|
resolved, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", UserID: "default-user", Purpose: CredentialPurposePersonalWrite,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("manual write resolution: %v", err)
|
|
}
|
|
if resolved.Principal.Kind != AuthPrincipalApp || resolved.Principal.Source != ConnectionSourceGitHubAppInstallation {
|
|
t.Fatalf("manual write principal = %+v, want App installation", resolved.Principal)
|
|
}
|
|
|
|
_, err = resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", UserID: "default-user", Purpose: CredentialPurposePersonalRead,
|
|
})
|
|
if !errors.Is(err, ErrGitHubConnectionInvalid) {
|
|
t.Fatalf("personal read error = %v, want reconnect-required invalid connection", err)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverGenerationSeparatesCachedClients(t *testing.T) {
|
|
connection := &WorkspaceConnection{WorkspaceID: "work", Source: ConnectionSourcePAT, Login: "first", Status: ConnectionStatusActive, CredentialGeneration: 1}
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{"work": connection}}
|
|
resolver := NewCredentialResolver(connections, fakeAuthSecrets{WorkspacePATSecretKey("work"): "pat"})
|
|
first, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "work", Purpose: CredentialPurposeAutomation})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection.CredentialGeneration = 2
|
|
connection.Login = "second"
|
|
second, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{WorkspaceID: "work", Purpose: CredentialPurposeAutomation})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Client == second.Client || second.Principal.Login != "second" {
|
|
t.Fatalf("generation did not replace client: first=%p second=%p principal=%+v", first.Client, second.Client, second.Principal)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverSeparatesRepositoryScopedAppTokens(t *testing.T) {
|
|
installationID := int64(42)
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"company": {
|
|
WorkspaceID: "company", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, AppRegistrationID: "registration-test",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
},
|
|
}}
|
|
provider := &repoScopedInstallationProvider{clients: make(map[string]Client)}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
resolver.SetInstallationProvider(provider)
|
|
first, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", Purpose: CredentialPurposeGitTransport,
|
|
RepoOwner: "acme", RepoName: "frontend",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", Purpose: CredentialPurposeGitTransport,
|
|
RepoOwner: "acme", RepoName: "backend",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", Purpose: CredentialPurposeGitTransport,
|
|
RepoOwner: "acme", RepoName: "frontend",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Client == second.Client || again.Client != first.Client || provider.calls != 2 {
|
|
t.Fatalf("clients = %p/%p/%p, provider calls = %d", first.Client, second.Client, again.Client, provider.calls)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverRefreshesCachedCredentialBeforeExpiry(t *testing.T) {
|
|
now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
|
|
installationID := int64(42)
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"company": {
|
|
WorkspaceID: "company", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, AppRegistrationID: "registration-test",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
},
|
|
}}
|
|
provider := &expiringInstallationProvider{now: func() time.Time { return now }, lifetime: 10 * time.Minute}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
resolver.now = func() time.Time { return now }
|
|
resolver.SetInstallationProvider(provider)
|
|
request := ResolveCredentialRequest{
|
|
WorkspaceID: "company", Purpose: CredentialPurposeAutomation,
|
|
RepoOwner: "acme", RepoName: "backend",
|
|
}
|
|
|
|
first, err := resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = now.Add(6 * time.Minute)
|
|
second, err := resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Client == second.Client || provider.calls != 2 {
|
|
t.Fatalf("clients = %p/%p, provider calls = %d", first.Client, second.Client, provider.calls)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverRejectsNewlyExpiredCredential(t *testing.T) {
|
|
now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
|
|
installationID := int64(42)
|
|
connections := &fakeConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"company": {
|
|
WorkspaceID: "company", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, AppRegistrationID: "registration-test",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
},
|
|
}}
|
|
provider := &expiringInstallationProvider{now: func() time.Time { return now }, lifetime: -time.Minute}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
resolver.now = func() time.Time { return now }
|
|
resolver.SetInstallationProvider(provider)
|
|
|
|
resolved, err := resolver.Resolve(context.Background(), ResolveCredentialRequest{
|
|
WorkspaceID: "company", Purpose: CredentialPurposeAutomation,
|
|
})
|
|
if resolved != nil || !errors.Is(err, ErrInstallationTokenExpired) {
|
|
t.Fatalf("Resolve() = %+v, %v; want nil, ErrInstallationTokenExpired", resolved, err)
|
|
}
|
|
}
|
|
|
|
func TestCredentialResolverInvalidationRejectsInflightOldGeneration(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
resolveNewFirst bool
|
|
}{
|
|
{name: "replacement resolution finishes before old resolution", resolveNewFirst: true},
|
|
{name: "old resolution finishes before replacement caller", resolveNewFirst: false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
installationID := int64(42)
|
|
oldConnection := WorkspaceConnection{
|
|
WorkspaceID: "company", Source: ConnectionSourceGitHubAppInstallation,
|
|
InstallationID: &installationID, AppRegistrationID: "registration-test",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
}
|
|
newConnection := oldConnection
|
|
newConnection.CredentialGeneration = 2
|
|
connections := &synchronizedConnectionReader{connection: oldConnection}
|
|
provider := &generationBarrierProvider{
|
|
oldStarted: make(chan struct{}), releaseOld: make(chan struct{}),
|
|
oldClient: NewMockClient(), newClient: NewMockClient(), calls: make(map[int64]int),
|
|
}
|
|
resolver := NewCredentialResolver(connections, nil)
|
|
resolver.SetInstallationProvider(provider)
|
|
request := ResolveCredentialRequest{WorkspaceID: "company", Purpose: CredentialPurposeAutomation}
|
|
oldResult := make(chan *ResolvedCredential, 1)
|
|
oldErr := make(chan error, 1)
|
|
go func() {
|
|
resolved, err := resolver.Resolve(context.Background(), request)
|
|
oldResult <- resolved
|
|
oldErr <- err
|
|
}()
|
|
<-provider.oldStarted
|
|
|
|
connections.replace(newConnection)
|
|
resolver.InvalidateWorkspace("company")
|
|
var replacement *ResolvedCredential
|
|
var err error
|
|
if test.resolveNewFirst {
|
|
replacement, err = resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
close(provider.releaseOld)
|
|
} else {
|
|
close(provider.releaseOld)
|
|
replacement = <-oldResult
|
|
if err = <-oldErr; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
var old *ResolvedCredential
|
|
if test.resolveNewFirst {
|
|
old = <-oldResult
|
|
if err = <-oldErr; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
} else {
|
|
old = replacement
|
|
replacement, err = resolver.Resolve(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if old == nil || replacement == nil || old.Client != provider.newClient ||
|
|
replacement.Client != provider.newClient {
|
|
t.Fatalf("old result = %+v, replacement = %+v", old, replacement)
|
|
}
|
|
provider.mu.Lock()
|
|
generationOneCalls := provider.calls[1]
|
|
provider.mu.Unlock()
|
|
if generationOneCalls != 1 {
|
|
t.Fatalf("generation 1 provider calls = %d, want 1", generationOneCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|