package github import ( "context" "errors" "sync" "testing" "time" ) type rateLimitSeedClient struct { *MockClient tracker *RateTracker calls int } func (c *rateLimitSeedClient) FetchRateLimit(context.Context) error { c.calls++ now := time.Now().UTC() for _, snapshot := range []RateSnapshot{ {Resource: ResourceCore, Remaining: 4321, Limit: 5000}, {Resource: ResourceGraphQL, Remaining: 4322, Limit: 5000}, {Resource: ResourceSearch, Remaining: 29, Limit: 30}, } { snapshot.ResetAt = now.Add(time.Hour) snapshot.UpdatedAt = now c.tracker.Record(snapshot) } return nil } type rateLimitAutomationProvider struct { client Client tracker *RateTracker } type concurrentRateLimitSeedClient struct { *MockClient tracker *RateTracker mu sync.Mutex calls int firstStarted chan struct{} secondStarted chan struct{} release chan struct{} } func (c *concurrentRateLimitSeedClient) FetchRateLimit(ctx context.Context) error { c.mu.Lock() c.calls++ call := c.calls c.mu.Unlock() switch call { case 1: close(c.firstStarted) case 2: close(c.secondStarted) } select { case <-c.release: case <-ctx.Done(): return ctx.Err() } now := time.Now().UTC() for _, snapshot := range []RateSnapshot{ {Resource: ResourceCore, Remaining: 4000 - call, Limit: 5000}, {Resource: ResourceGraphQL, Remaining: 4001 - call, Limit: 5000}, {Resource: ResourceSearch, Remaining: 28 - call, Limit: 30}, } { snapshot.ResetAt = now.Add(time.Hour) snapshot.UpdatedAt = now c.tracker.Record(snapshot) } return nil } func (c *concurrentRateLimitSeedClient) callCount() int { c.mu.Lock() defer c.mu.Unlock() return c.calls } func (p rateLimitAutomationProvider) ResolveAutomation( context.Context, *WorkspaceConnection, ResolveCredentialRequest, ) (*ResolvedCredential, error) { return &ResolvedCredential{ Client: p.client, Capabilities: allTokenCapabilities(), Principal: AuthPrincipal{ Kind: AuthPrincipalHuman, Source: ConnectionSourceGHCLI, Login: "octocat", }, RateTracker: p.tracker, }, nil } func TestWorkspaceAuthStatusSeedsRateLimitOncePerCachedCredential(t *testing.T) { store := newTestStore(t) seedConnectionWorkspaces(t, store, "workspace-rate-limit") connection := &WorkspaceConnection{ WorkspaceID: "workspace-rate-limit", Source: ConnectionSourceGHCLI, GitHubHost: defaultGitHubHost, Login: "octocat", Status: ConnectionStatusActive, CredentialGeneration: 1, } if err := store.UpsertWorkspaceConnection(context.Background(), connection); err != nil { t.Fatalf("seed workspace connection: %v", err) } tracker := NewRateTracker(nil, nil) client := &rateLimitSeedClient{MockClient: NewMockClient(), tracker: tracker} service := NewService(client, AuthMethodPAT, nil, store, nil, testLogger(t)) service.resolver = NewCredentialResolver(testConnectionReader{ workspaces: map[string]*WorkspaceConnection{connection.WorkspaceID: connection}, }, nil) service.resolver.SetAutomationProvider(rateLimitAutomationProvider{client: client, tracker: tracker}) for i := 0; i < 2; i++ { status, err := service.GetWorkspaceAuthStatus(context.Background(), connection.WorkspaceID, DefaultUserID) if err != nil { t.Fatalf("GetWorkspaceAuthStatus call %d: %v", i+1, err) } if status.RateLimit == nil || status.RateLimit.Core == nil { t.Fatalf("call %d rate limit = %+v, want seeded core snapshot", i+1, status.RateLimit) } if status.RateLimit.Core.Remaining != 4321 { t.Errorf("call %d remaining = %d, want 4321", i+1, status.RateLimit.Core.Remaining) } } if client.calls != 1 { t.Fatalf("FetchRateLimit calls = %d, want 1 for cached credential", client.calls) } status, err := service.RefreshWorkspaceRateLimit( context.Background(), connection.WorkspaceID, DefaultUserID, ) if err != nil { t.Fatalf("RefreshWorkspaceRateLimit: %v", err) } if status.RateLimit == nil || status.RateLimit.Core == nil { t.Fatalf("forced refresh rate limit = %+v, want seeded core snapshot", status.RateLimit) } if client.calls != 2 { t.Fatalf("FetchRateLimit calls after forced refresh = %d, want 2", client.calls) } } func TestWorkspaceAuthStatusCompletesPartialRateLimitSeed(t *testing.T) { store := newTestStore(t) seedConnectionWorkspaces(t, store, "workspace-partial-rate-limit") connection := &WorkspaceConnection{ WorkspaceID: "workspace-partial-rate-limit", Source: ConnectionSourceGHCLI, GitHubHost: defaultGitHubHost, Login: "octocat", Status: ConnectionStatusActive, CredentialGeneration: 1, } if err := store.UpsertWorkspaceConnection(context.Background(), connection); err != nil { t.Fatalf("seed workspace connection: %v", err) } tracker := NewRateTracker(nil, nil) tracker.Record(RateSnapshot{ Resource: ResourceCore, Remaining: 4999, Limit: 5000, ResetAt: time.Now().Add(time.Hour).UTC(), UpdatedAt: time.Now().UTC(), }) client := &rateLimitSeedClient{MockClient: NewMockClient(), tracker: tracker} service := NewService(client, AuthMethodPAT, nil, store, nil, testLogger(t)) service.resolver = NewCredentialResolver(testConnectionReader{ workspaces: map[string]*WorkspaceConnection{connection.WorkspaceID: connection}, }, nil) service.resolver.SetAutomationProvider(rateLimitAutomationProvider{client: client, tracker: tracker}) status, err := service.GetWorkspaceAuthStatus( context.Background(), connection.WorkspaceID, DefaultUserID, ) if err != nil { t.Fatalf("GetWorkspaceAuthStatus: %v", err) } if client.calls != 1 { t.Fatalf("FetchRateLimit calls = %d, want 1 for a partial tracker", client.calls) } if status.RateLimit == nil || status.RateLimit.GraphQL == nil || status.RateLimit.Search == nil { t.Fatalf("rate limit = %+v, want all seeded buckets", status.RateLimit) } } func TestWorkspaceRateLimitSeedSerializesConcurrentRefreshes(t *testing.T) { store := newTestStore(t) seedConnectionWorkspaces(t, store, "workspace-concurrent-rate-limit") connection := &WorkspaceConnection{ WorkspaceID: "workspace-concurrent-rate-limit", Source: ConnectionSourceGHCLI, GitHubHost: defaultGitHubHost, Login: "octocat", Status: ConnectionStatusActive, CredentialGeneration: 1, } if err := store.UpsertWorkspaceConnection(context.Background(), connection); err != nil { t.Fatalf("seed workspace connection: %v", err) } tracker := NewRateTracker(nil, nil) client := &concurrentRateLimitSeedClient{ MockClient: NewMockClient(), tracker: tracker, firstStarted: make(chan struct{}), secondStarted: make(chan struct{}), release: make(chan struct{}), } service := NewService(client, AuthMethodPAT, nil, store, nil, testLogger(t)) service.resolver = NewCredentialResolver(testConnectionReader{ workspaces: map[string]*WorkspaceConnection{connection.WorkspaceID: connection}, }, nil) service.resolver.SetAutomationProvider(rateLimitAutomationProvider{client: client, tracker: tracker}) regularDone := make(chan error, 1) go func() { _, err := service.GetWorkspaceAuthStatus(context.Background(), connection.WorkspaceID, DefaultUserID) regularDone <- err }() <-client.firstStarted forcedDone := make(chan error, 1) go func() { _, err := service.RefreshWorkspaceRateLimit(context.Background(), connection.WorkspaceID, DefaultUserID) forcedDone <- err }() secondStarted := false select { case <-client.secondStarted: secondStarted = true case <-time.After(100 * time.Millisecond): } close(client.release) if err := <-regularDone; err != nil { t.Fatalf("regular status: %v", err) } if err := <-forcedDone; err != nil { t.Fatalf("forced status: %v", err) } if secondStarted { t.Fatal("concurrent forced refresh started a duplicate rate-limit fetch") } if got := client.callCount(); got != 1 { t.Fatalf("FetchRateLimit calls = %d, want 1", got) } } func TestAppAuthRegistryHotAddsAndInvalidatesRegistrationsIndependently(t *testing.T) { store := newTestStore(t) service := NewService(nil, AuthMethodNone, nil, store, nil, testLogger(t)) service.SetConnectionSecretStore(newFakeConnectionSecrets()) first := testResolvedAppRegistration(t, "registration-a", 101, 1) second := testResolvedAppRegistration(t, "registration-b", 202, 3) if err := service.ApplyAppRegistrationRuntime(first); err != nil { t.Fatalf("ApplyAppRegistrationRuntime(first): %v", err) } if err := service.ApplyAppRegistrationRuntime(second); err != nil { t.Fatalf("ApplyAppRegistrationRuntime(second): %v", err) } if got := service.AppRegistrationRuntimeSnapshot("registration-a"); !got.Ready || got.AppID != 101 || got.Generation != 1 { t.Fatalf("first snapshot = %+v", got) } if got := service.AppRegistrationRuntimeSnapshot("registration-b"); !got.Ready || got.AppID != 202 || got.Generation != 3 { t.Fatalf("second snapshot = %+v", got) } service.InvalidateAppRegistrationRuntime("registration-a", 1) if got := service.AppRegistrationRuntimeSnapshot("registration-a"); got.Ready { t.Fatalf("invalidated snapshot = %+v", got) } if got := service.AppRegistrationRuntimeSnapshot("registration-b"); !got.Ready { t.Fatalf("unrelated snapshot = %+v", got) } } func TestAppAuthRegistryRejectsOneRegistrationWithoutReplacingAnother(t *testing.T) { store := newTestStore(t) service := NewService(nil, AuthMethodNone, nil, store, nil, testLogger(t)) service.SetConnectionSecretStore(newFakeConnectionSecrets()) valid := testResolvedAppRegistration(t, "registration-a", 101, 1) if err := service.ApplyAppRegistrationRuntime(valid); err != nil { t.Fatal(err) } invalid := testResolvedAppRegistration(t, "registration-b", 202, 1) invalid.Config.PrivateKey = "not a private key" if err := service.ApplyAppRegistrationRuntime(invalid); err == nil { t.Fatal("invalid registration unexpectedly loaded") } if got := service.AppRegistrationRuntimeSnapshot("registration-a"); !got.Ready { t.Fatalf("valid registration was removed: %+v", got) } if got := service.AppRegistrationRuntimeSnapshot("registration-b"); got.Ready { t.Fatalf("invalid registration became ready: %+v", got) } } func TestAppAuthRegistryStaleInvalidationDoesNotRemoveNewGeneration(t *testing.T) { store := newTestStore(t) service := NewService(nil, AuthMethodNone, nil, store, nil, testLogger(t)) service.SetConnectionSecretStore(newFakeConnectionSecrets()) if err := service.ApplyAppRegistrationRuntime(testResolvedAppRegistration(t, "registration-a", 101, 2)); err != nil { t.Fatal(err) } service.InvalidateAppRegistrationRuntime("registration-a", 1) if got := service.AppRegistrationRuntimeSnapshot("registration-a"); !got.Ready || got.Generation != 2 { t.Fatalf("new generation was removed by stale invalidation: %+v", got) } } func testResolvedAppRegistration( t *testing.T, registrationID string, appID, generation int64, ) ResolvedDeploymentAppConfig { t.Helper() _, privateKey := testAppPrivateKey(t) return ResolvedDeploymentAppConfig{ Source: DeploymentAppSourceManaged, Registration: &AppRegistration{ ID: registrationID, Source: AppRegistrationSourceManaged, DisplayName: registrationID, GitHubHost: "github.com", AppID: appID, ClientID: "Iv1.client", Slug: "kandev-app", OwnerLogin: "acme", OwnerType: AppRegistrationOwnerOrganization, Visibility: AppRegistrationVisibilityPrivate, PublicBaseURL: "https://kandev.example", CredentialGeneration: generation, CredentialSecretID: "github:app-registration:" + registrationID + ":test", Status: AppRegistrationStatusActive, WebhookStatus: DeploymentAppWebhookUnverified, }, Config: AppRegistrationRuntimeConfig{ AppID: appID, ClientID: "Iv1.client", ClientSecret: "client-secret", PrivateKey: string(privateKey), WebhookSecret: "webhook-secret", Slug: "kandev-app", PublicBaseURL: "https://kandev.example", }, } } func TestAppAuthRegistryLoadsValidRegistrationsWhenAnotherFails(t *testing.T) { store := newTestStore(t) secrets := newFakeConnectionSecrets() repository := NewAppRegistrationRepository(store, secrets) valid := testResolvedAppRegistration(t, "registration-a", 101, 1) if err := repository.SaveRegistration(context.Background(), valid.Registration, DeploymentAppCredentials{ PrivateKey: valid.Config.PrivateKey, ClientSecret: valid.Config.ClientSecret, WebhookSecret: valid.Config.WebhookSecret, }); err != nil { t.Fatal(err) } invalid := *valid.Registration invalid.ID = "registration-b" invalid.AppID = 202 invalid.CredentialGeneration = 1 if err := repository.SaveRegistration(context.Background(), &invalid, DeploymentAppCredentials{ PrivateKey: "invalid", ClientSecret: "client-secret", WebhookSecret: "webhook-secret", }); err != nil { t.Fatal(err) } service := NewService(nil, AuthMethodNone, nil, store, nil, testLogger(t)) service.SetConnectionSecretStore(secrets) err := service.InitializeAppRegistrationRuntimes(context.Background()) if err == nil { t.Fatal("startup did not report invalid registration") } if got := service.AppRegistrationRuntimeSnapshot("registration-a"); !got.Ready { t.Fatalf("valid runtime not loaded: %+v", got) } if got := service.AppRegistrationRuntimeSnapshot("registration-b"); got.Ready { t.Fatalf("invalid runtime loaded: %+v", got) } } func TestAppAuthRegistrySkipsPersistedInvalidRegistration(t *testing.T) { store := newTestStore(t) secrets := newFakeConnectionSecrets() repository := NewAppRegistrationRepository(store, secrets) active := testResolvedAppRegistration(t, "registration-active", 101, 1) invalid := testResolvedAppRegistration(t, "registration-invalid", 202, 1) invalid.Registration.Status = AppRegistrationStatusInvalid for _, resolved := range []ResolvedDeploymentAppConfig{active, invalid} { if err := repository.SaveRegistration(context.Background(), resolved.Registration, DeploymentAppCredentials{ PrivateKey: resolved.Config.PrivateKey, ClientSecret: resolved.Config.ClientSecret, WebhookSecret: resolved.Config.WebhookSecret, }); err != nil { t.Fatal(err) } } service := NewService(nil, AuthMethodNone, nil, store, nil, testLogger(t)) service.SetConnectionSecretStore(secrets) if err := service.InitializeAppRegistrationRuntimes(context.Background()); err == nil { t.Fatal("startup did not report invalid registration") } if got := service.AppRegistrationRuntimeSnapshot(active.Registration.ID); !got.Ready { t.Fatalf("active registration was not loaded: %+v", got) } if got := service.AppRegistrationRuntimeSnapshot(invalid.Registration.ID); got.Ready { t.Fatalf("invalid registration was loaded: %+v", got) } if err := service.ApplyAppRegistrationRuntime(invalid); err == nil { t.Fatalf("ApplyAppRegistrationRuntime(invalid) error = %v", err) } } func TestWorkspaceAuthStatusUsesSelectedAppRegistration(t *testing.T) { store := newTestStore(t) seedConnectionWorkspaces(t, store, "ws-a", "ws-b", "ws-invalid") service := NewService(nil, AuthMethodNone, nil, store, nil, testLogger(t)) service.SetConnectionSecretStore(newFakeConnectionSecrets()) activeA := testResolvedAppRegistration(t, "registration-a", 101, 1) activeB := testResolvedAppRegistration(t, "registration-b", 202, 1) invalid := testResolvedAppRegistration(t, "registration-invalid", 303, 1) invalid.Registration.Status = AppRegistrationStatusInvalid for _, resolved := range []ResolvedDeploymentAppConfig{activeA, activeB} { if err := service.ApplyAppRegistrationRuntime(resolved); err != nil { t.Fatal(err) } } for _, registration := range []*AppRegistration{ activeA.Registration, activeB.Registration, invalid.Registration, } { if err := store.UpsertDeploymentAppRegistration(context.Background(), registration); err != nil { t.Fatal(err) } } installationID := int64(42) for workspaceID, registrationID := range map[string]string{ "ws-a": "registration-a", "ws-b": "registration-b", "ws-invalid": "registration-invalid", } { if err := store.UpsertWorkspaceConnection(context.Background(), &WorkspaceConnection{ WorkspaceID: workspaceID, Source: ConnectionSourceGitHubAppInstallation, GitHubHost: defaultGitHubHost, InstallationID: &installationID, InstallationAccountLogin: "acme", InstallationAccountType: "Organization", AppRegistrationID: registrationID, Status: ConnectionStatusActive, CredentialGeneration: 1, }); err != nil { t.Fatal(err) } } for workspaceID, registrationID := range map[string]string{"ws-a": "registration-a", "ws-b": "registration-b"} { status, err := service.GetWorkspaceAuthStatus(context.Background(), workspaceID, DefaultUserID) if err != nil { t.Fatal(err) } if status.AppRegistration == nil || status.AppRegistration.ID != registrationID || !status.GitHubAppAvailable || !status.AppAvailable { t.Fatalf("status for %s = %+v", workspaceID, status) } } status, err := service.GetWorkspaceAuthStatus(context.Background(), "ws-invalid", DefaultUserID) if err != nil { t.Fatal(err) } if status.AppRegistration == nil || status.AppRegistration.ID != "registration-invalid" || status.GitHubAppAvailable || status.AppAvailable { t.Fatalf("invalid registration status = %+v", status) } } func TestServicePreparesImportAndRejectsWrongBinding(t *testing.T) { _, service, store := setupAppRegistrationController(t) seedConnectionWorkspaces(t, store, "workspace-1", "workspace-2") prepared, err := service.PrepareAppRegistrationImport( context.Background(), DefaultUserID, AppRegistrationImportPrepareRequest{ WorkspaceID: "workspace-1", PublicBaseURL: "https://kandev.example", }, ) if err != nil { t.Fatal(err) } if prepared.RegistrationID == "" || prepared.PublicBaseURL != "https://kandev.example" || prepared.SetupURL != "" { t.Fatalf("prepared import = %+v", prepared) } _, err = service.ImportAppRegistration(context.Background(), DefaultUserID, AppRegistrationImportRequest{ RegistrationID: prepared.RegistrationID, WorkspaceID: "workspace-2", PublicBaseURL: prepared.PublicBaseURL, }) if !errors.Is(err, ErrAppRegistrationImportPreparationUnavailable) { t.Fatalf("wrong workspace import error = %v", err) } stored, err := store.GetAppRegistrationImportPreparation(context.Background(), prepared.RegistrationID) if err != nil || stored == nil || stored.ConsumedAt != nil { t.Fatalf("wrong binding consumed preparation = %+v, err %v", stored, err) } }