package identity import ( "context" "encoding/json" "errors" "strings" "sync" "testing" "time" ) type pairingRepositoryFake struct { revision Revision exchange PairingExchange blocked bool startReservationErr error startReservationID string startReservationState string startReservationCalls int startReservationReleases int pendingSecretCleanups map[string]time.Time commitStartErr error commitStartAmbiguous bool applyManifestAmbiguousOnce bool failCredentialsSavedOnce bool } func (f *pairingRepositoryFake) ReserveIdentityPairingStart(_ context.Context, attemptID string, _ time.Time) error { f.startReservationCalls++ if f.startReservationErr != nil { return f.startReservationErr } if f.blocked || f.startReservationID != "" { return ErrPairingInProgress } f.startReservationID = attemptID f.startReservationState = "starting" return nil } func (f *pairingRepositoryFake) ReleaseIdentityPairingStart(_ context.Context, attemptID string) error { if f.startReservationID == attemptID && f.startReservationState == "starting" { f.startReservationID = "" f.startReservationState = "" f.startReservationReleases++ } return nil } func (f *pairingRepositoryFake) CommitIdentityPairingStart(_ context.Context, revision Revision, exchange PairingExchange) (PairingExchange, error) { if f.commitStartErr != nil && !f.commitStartAmbiguous { return PairingExchange{}, f.commitStartErr } if f.startReservationID != exchange.ID || f.startReservationState != "starting" { return PairingExchange{}, ErrPairingInProgress } if _, staged := f.pendingSecretCleanups[exchange.ExchangeTokenRef]; !staged { return PairingExchange{}, errors.New("exchange token was not staged") } delete(f.pendingSecretCleanups, exchange.ExchangeTokenRef) f.revision = revision f.exchange = exchange f.startReservationState = "paired" if f.commitStartAmbiguous { return PairingExchange{}, f.commitStartErr } return exchange, nil } func (f *pairingRepositoryFake) CreateIdentityConfigurationRevision(_ context.Context, revision Revision) (Revision, error) { f.revision = revision return revision, nil } func (f *pairingRepositoryFake) IdentityConfigurationRevision(context.Context, string) (Revision, error) { return f.revision, nil } func (f *pairingRepositoryFake) ApplyIdentityManifest(_ context.Context, _ string, _ int64, applied ManifestApplication) (Revision, error) { for _, reference := range []string{applied.MachineCredentialRef, applied.SessionEncryptionKeyRef} { if reference == "" { continue } if _, staged := f.pendingSecretCleanups[reference]; !staged { return Revision{}, errors.New("identity secret was not staged") } } oldMachineReference, oldSessionReference := f.revision.MachineCredentialRef, f.revision.SessionEncryptionKeyRef revision, err := ApplyManifest(f.revision, applied) if err != nil { return Revision{}, err } revision.Version++ f.revision = revision for _, reference := range []string{applied.MachineCredentialRef, applied.SessionEncryptionKeyRef} { delete(f.pendingSecretCleanups, reference) } for _, reference := range []string{oldMachineReference, oldSessionReference} { if reference != "" && reference != applied.MachineCredentialRef && reference != applied.SessionEncryptionKeyRef { f.pendingSecretCleanups[reference] = time.Now() } } if f.applyManifestAmbiguousOnce { f.applyManifestAmbiguousOnce = false return Revision{}, errors.New("manifest commit response lost") } return revision, nil } func (f *pairingRepositoryFake) CreateIdentityPairingExchange(_ context.Context, exchange PairingExchange) (PairingExchange, error) { f.exchange = exchange return exchange, nil } func (f *pairingRepositoryFake) IdentityPairingExchange(context.Context, string) (PairingExchange, error) { return f.exchange, nil } func (f *pairingRepositoryFake) UpdateIdentityPairingExchange(_ context.Context, id string, expected int64, update PairingExchangeUpdate) (PairingExchange, error) { if f.exchange.ID != id || f.exchange.Version != expected { return PairingExchange{}, ErrRevisionConflict } if update.Status == PairingCredentialsSaved && f.failCredentialsSavedOnce { f.failCredentialsSavedOnce = false return PairingExchange{}, errors.New("pairing status temporarily unavailable") } f.exchange.Status, f.exchange.RemoteVersion = update.Status, update.RemoteVersion f.exchange.LastErrorCategory, f.exchange.AuthCenterAuditID = update.LastErrorCategory, update.AuthCenterAuditID f.exchange.Version++ if update.Status == PairingCompleted || update.Status == PairingFailed || update.Status == PairingExpired { if f.pendingSecretCleanups == nil { f.pendingSecretCleanups = map[string]time.Time{} } f.pendingSecretCleanups[f.exchange.ExchangeTokenRef] = time.Now() } return f.exchange, nil } func (f *pairingRepositoryFake) RecordIdentityPairingRetryFailure(_ context.Context, id string, status PairingStatus, category string) (PairingExchange, error) { if f.exchange.ID != id || f.exchange.Status != status { return PairingExchange{}, ErrRevisionConflict } f.exchange.LastErrorCategory = category return f.exchange, nil } func (f *pairingRepositoryFake) IdentityPairingStartBlocked(context.Context) (bool, error) { return f.blocked, nil } func (f *pairingRepositoryFake) QueueIdentitySecretCleanup(_ context.Context, reference string, notBefore time.Time) error { if f.pendingSecretCleanups == nil { f.pendingSecretCleanups = map[string]time.Time{} } current, exists := f.pendingSecretCleanups[reference] if !exists || notBefore.Before(current) { f.pendingSecretCleanups[reference] = notBefore } return nil } func (f *pairingRepositoryFake) RemovePendingIdentitySecretCleanup(_ context.Context, reference string) (bool, error) { if _, exists := f.pendingSecretCleanups[reference]; !exists { return false, nil } delete(f.pendingSecretCleanups, reference) return true, nil } func (f *pairingRepositoryFake) CancelIdentityPairingExchange(_ context.Context, id string, expected int64, traceID, auditID string) (PairingExchange, error) { if f.exchange.ID != id || f.revision.State == RevisionActive || f.revision.State == RevisionSuperseded { return PairingExchange{}, ErrRevisionConflict } if f.exchange.Status == PairingCancelled { return f.exchange, nil } if f.exchange.Version != expected { return PairingExchange{}, ErrRevisionConflict } f.exchange.Status = PairingCancelled f.exchange.CleanupStatus = PairingCleanupPending f.exchange.Version++ now := time.Now().UTC() f.exchange.CancelledAt = &now f.revision.State = RevisionFailed f.revision.LastErrorCategory = "pairing_cancelled" f.revision.LastTraceID = traceID f.revision.LastAuditID = auditID f.revision.Version++ if f.exchange.ExchangeTokenRef != "" { if f.pendingSecretCleanups == nil { f.pendingSecretCleanups = map[string]time.Time{} } f.pendingSecretCleanups[f.exchange.ExchangeTokenRef] = time.Now() } return f.exchange, nil } func (f *pairingRepositoryFake) CompleteIdentityPairingCleanup(_ context.Context, id string, expected int64) (PairingExchange, error) { if f.exchange.ID != id || f.exchange.Version != expected || f.exchange.Status != PairingCancelled || f.exchange.CleanupStatus != PairingCleanupPending { return PairingExchange{}, ErrRevisionConflict } f.exchange.CleanupStatus = PairingCleanupCompleted f.exchange.Version++ now := time.Now().UTC() f.exchange.CleanupCompletedAt = &now f.revision.MachineCredentialRef = "" f.revision.SessionEncryptionKeyRef = "" if f.startReservationState == "paired" && f.startReservationID == f.exchange.ID { f.startReservationID = "" f.startReservationState = "" } return f.exchange, nil } func (f *pairingRepositoryFake) RecordIdentityPairingCleanupFailure(_ context.Context, id string, category string) (PairingExchange, error) { if f.exchange.ID != id || f.exchange.Status != PairingCancelled || f.exchange.CleanupStatus != PairingCleanupPending { return PairingExchange{}, ErrRevisionConflict } f.exchange.LastErrorCategory = category return f.exchange, nil } type secretStoreFake struct { values map[string][]byte failMachine bool } func (f *secretStoreFake) Put(_ context.Context, reference string, value []byte) error { if f.failMachine && len(reference) > len("identity-machine-") && reference[:len("identity-machine-")] == "identity-machine-" { return errors.New("secret store unavailable") } if f.values == nil { f.values = map[string][]byte{} } f.values[reference] = append([]byte(nil), value...) return nil } func (f *secretStoreFake) Get(_ context.Context, reference string) ([]byte, error) { value, ok := f.values[reference] if !ok { return nil, errors.New("secret not found") } return append([]byte(nil), value...), nil } func (f *secretStoreFake) Delete(_ context.Context, reference string) error { delete(f.values, reference) return nil } type onboardingRemoteFake struct { claimed ClaimedExchange claimErr error claimCalls int view Exchange delivery CredentialDelivery deliveryCalls int completed bool completeErr error } func (f *onboardingRemoteFake) Claim(context.Context, string) (ClaimedExchange, error) { f.claimCalls++ return f.claimed, f.claimErr } func (f *onboardingRemoteFake) SubmitMetadata(context.Context, ClaimedExchange, ConsumerMetadata, string) (Exchange, error) { f.view.Status, f.view.Version = ExchangePreparing, 2 return f.view, nil } func (f *onboardingRemoteFake) Get(context.Context, string, string) (Exchange, error) { f.view.Status, f.view.Version = ExchangeReady, 3 return f.view, nil } func (f *onboardingRemoteFake) DeliverCredential(context.Context, Exchange, string, string) (CredentialDelivery, error) { f.deliveryCalls++ f.delivery.MachineCredential.ClientSecret = "rotated-machine-secret-value-000000000000-" + string(rune('0'+f.deliveryCalls)) f.delivery.Version = int64(3 + f.deliveryCalls) return f.delivery, nil } func (f *onboardingRemoteFake) Complete(context.Context, string, string, string, int64) error { f.completed = true return f.completeErr } type securityEventPreparerFake struct { called bool err error cleanupCalled bool cleanupErr error conflictRetireCalled bool conflictRetireErr error conflictRevisionID string } type blockingSecurityEventPreparer struct { prepareStarted chan struct{} releasePrepare chan struct{} cleanupStarted chan struct{} cleanupOnce sync.Once } func (preparer *blockingSecurityEventPreparer) PrepareSecurityEvents(context.Context, Revision, []byte) error { close(preparer.prepareStarted) <-preparer.releasePrepare return nil } func (preparer *blockingSecurityEventPreparer) CleanupPreparedSecurityEvents(context.Context, Revision) error { preparer.cleanupOnce.Do(func() { close(preparer.cleanupStarted) }) return nil } func (f *securityEventPreparerFake) PrepareSecurityEvents(context.Context, Revision, []byte) error { f.called = true return f.err } func (f *securityEventPreparerFake) CleanupPreparedSecurityEvents(context.Context, Revision) error { f.cleanupCalled = true return f.cleanupErr } func (f *securityEventPreparerFake) RetireConflictingSecurityEvents(_ context.Context, revision Revision) error { f.conflictRetireCalled = true f.conflictRevisionID = revision.ID return f.conflictRetireErr } type safeCategoryTestError struct { category string message string } func (err safeCategoryTestError) Error() string { return err.message } func (err safeCategoryTestError) SafeErrorCategory() string { return err.category } func TestPairingStartReservationSerializesAndRecoversFailedClaims(t *testing.T) { input := PairingInput{ AuthCenterURL: "https://auth.example.com", OnboardingCode: "one-time-code-value", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", } t.Run("existing pairing blocks before remote claim", func(t *testing.T) { repository := &pairingRepositoryFake{blocked: true} remote := &onboardingRemoteFake{} service := NewPairingService(repository, &secretStoreFake{}, func(string) (OnboardingRemote, error) { return remote, nil }, nil) if _, err := service.Start(context.Background(), input, "trace-blocked"); !errors.Is(err, ErrPairingInProgress) { t.Fatalf("blocked Start error=%v", err) } if repository.startReservationCalls != 1 || repository.startReservationReleases != 0 || repository.startReservationID != "" || remote.claimCalls != 0 { t.Fatalf("reservation/claim lifecycle calls=%d releases=%d id=%q claims=%d", repository.startReservationCalls, repository.startReservationReleases, repository.startReservationID, remote.claimCalls) } }) t.Run("active machine credential blocks destructive rotation before remote claim", func(t *testing.T) { repository := &pairingRepositoryFake{startReservationErr: ErrActiveConfigurationHandoffRequired} remote := &onboardingRemoteFake{} service := NewPairingService(repository, &secretStoreFake{}, func(string) (OnboardingRemote, error) { return remote, nil }, nil) if _, err := service.Start(context.Background(), input, "trace-active"); !errors.Is(err, ErrActiveConfigurationHandoffRequired) { t.Fatalf("active credential guard error=%v", err) } if remote.claimCalls != 0 { t.Fatal("active credential guard ran after the one-time onboarding code was claimed") } }) t.Run("remote failure releases database guard", func(t *testing.T) { repository := &pairingRepositoryFake{} remote := &onboardingRemoteFake{claimErr: errors.New("remote unavailable")} service := NewPairingService(repository, &secretStoreFake{}, func(string) (OnboardingRemote, error) { return remote, nil }, nil) if _, err := service.Start(context.Background(), input, "trace-failed"); err == nil { t.Fatal("remote claim failure was ignored") } if repository.startReservationCalls != 1 || repository.startReservationReleases != 1 || repository.startReservationID != "" || remote.claimCalls != 1 { t.Fatalf("reservation was not released after remote failure: %#v remote=%#v", repository, remote) } }) } func TestPairingStartLeavesUncommittedExchangeTokenInDurableCleanupQueue(t *testing.T) { repository := &pairingRepositoryFake{commitStartErr: errors.New("database commit unavailable")} secrets := &secretStoreFake{values: map[string][]byte{}} remote := &onboardingRemoteFake{claimed: ClaimedExchange{ ExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeToken: "exchange-token-value", Version: 1, ExpiresAt: time.Now().Add(time.Hour), }} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, nil) if _, err := service.Start(context.Background(), PairingInput{ AuthCenterURL: "https://auth.example.com", OnboardingCode: "one-time-code-value", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", }, "trace-uncommitted"); err == nil { t.Fatal("failed pairing commit was ignored") } if len(secrets.values) != 1 || len(repository.pendingSecretCleanups) != 1 || repository.startReservationID != "" || repository.startReservationReleases != 1 { t.Fatalf("uncommitted token was not left recoverable: secrets=%d cleanup=%d reservation=%q releases=%d", len(secrets.values), len(repository.pendingSecretCleanups), repository.startReservationID, repository.startReservationReleases) } for reference := range secrets.values { if _, queued := repository.pendingSecretCleanups[reference]; !queued { t.Fatalf("uncommitted Secret reference %q is not queued for cleanup", reference) } } } func TestPairingStartAmbiguousCommitNeverRequeuesAdoptedExchangeToken(t *testing.T) { repository := &pairingRepositoryFake{ commitStartErr: errors.New("commit response lost"), commitStartAmbiguous: true, } secrets := &secretStoreFake{values: map[string][]byte{}} remote := &onboardingRemoteFake{claimed: ClaimedExchange{ ExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeToken: "exchange-token-value", Version: 1, ExpiresAt: time.Now().Add(time.Hour), }} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, nil) if _, err := service.Start(context.Background(), PairingInput{ AuthCenterURL: "https://auth.example.com", OnboardingCode: "one-time-code-value", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", }, "trace-ambiguous"); err == nil { t.Fatal("ambiguous pairing commit was not reported") } if repository.exchange.ID == "" || repository.revision.ID == "" { t.Fatal("test did not simulate a committed Pairing") } if len(secrets.values) != 1 || len(repository.pendingSecretCleanups) != 0 { t.Fatalf("adopted exchange token was re-queued: secrets=%d cleanup=%d", len(secrets.values), len(repository.pendingSecretCleanups)) } if repository.startReservationID != repository.exchange.ID || repository.startReservationState != "paired" || repository.startReservationReleases != 0 { t.Fatalf("committed reservation was not retained: id=%q state=%q releases=%d", repository.startReservationID, repository.startReservationState, repository.startReservationReleases) } } func TestPairingRetriesWithRotatedCredentialAfterSecretStoreFailure(t *testing.T) { repository := &pairingRepositoryFake{} secrets := &secretStoreFake{values: map[string][]byte{}, failMachine: true} remote := &onboardingRemoteFake{ claimed: ClaimedExchange{ExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeToken: "exchange-token-value", Version: 1, ExpiresAt: time.Now().Add(time.Hour)}, view: Exchange{ExchangeID: "11111111-1111-1111-1111-111111111111", ApplicationID: "22222222-2222-2222-2222-222222222222", Version: 1, ExpiresAt: time.Now().Add(time.Hour)}, delivery: CredentialDelivery{Manifest: ManifestV1{ SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/shared", TenantID: "33333333-3333-3333-3333-333333333333", ApplicationID: "22222222-2222-2222-2222-222222222222", Audience: "urn:easyai:resource:22222222-2222-2222-2222-222222222222", Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"}, Scopes: []string{"openid", "gateway.access"}, Clients: ManifestClients{BrowserLogin: &ManifestClient{ClientID: "browser"}, MachineToMachine: &ManifestClient{ClientID: "service"}}, SecurityEvents: &ManifestSecurityEvents{TransmitterIssuer: "https://auth.example.com/ssf", ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf", Audience: "urn:easyai:ssf:receiver:22222222-2222-2222-2222-222222222222"}, }, MachineCredential: &MachineCredential{ClientID: "service", ClientSecret: "placeholder", IssuedAt: time.Now()}}, } preparer := &securityEventPreparerFake{} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, preparer) pairing, err := service.Start(context.Background(), PairingInput{ AuthCenterURL: "https://auth.example.com", OnboardingCode: "one-time-code-value", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", }, "trace-pairing") if err != nil { t.Fatal(err) } if _, ok := secrets.values[pairing.ExchangeTokenRef]; !ok { t.Fatal("exchange token was not saved in SecretStore") } if _, err := service.Continue(context.Background(), pairing.ID); err == nil { t.Fatal("machine SecretStore failure was ignored") } if remote.completed || repository.revision.ApplicationID != "" { t.Fatal("failed secret save advanced the exchange or revision") } secrets.failMachine = false completed, err := service.Continue(context.Background(), pairing.ID) if err != nil { t.Fatal(err) } if !remote.completed || completed.Status != PairingCompleted || remote.deliveryCalls != 2 || !preparer.called { t.Fatalf("pairing did not recover safely: pairing=%#v calls=%d completed=%v", completed, remote.deliveryCalls, remote.completed) } if repository.revision.MachineCredentialRef == "" || repository.revision.SessionEncryptionKeyRef == "" { t.Fatalf("revision did not retain SecretStore references: %#v", repository.revision) } } func TestPairingRetryNeverOverwritesOrDeletesAdoptedIdentitySecrets(t *testing.T) { repository := &pairingRepositoryFake{applyManifestAmbiguousOnce: true, failCredentialsSavedOnce: true} secrets := &secretStoreFake{values: map[string][]byte{}} remote := &onboardingRemoteFake{ claimed: ClaimedExchange{ExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeToken: "exchange-token-value", Version: 1, ExpiresAt: time.Now().Add(time.Hour)}, view: Exchange{ExchangeID: "11111111-1111-1111-1111-111111111111", ApplicationID: "22222222-2222-2222-2222-222222222222", Version: 1, ExpiresAt: time.Now().Add(time.Hour)}, delivery: CredentialDelivery{Manifest: ManifestV1{ SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/shared", TenantID: "33333333-3333-3333-3333-333333333333", ApplicationID: "22222222-2222-2222-2222-222222222222", Audience: "urn:easyai:resource:22222222-2222-2222-2222-222222222222", Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"}, Scopes: []string{"openid", "gateway.access"}, Clients: ManifestClients{BrowserLogin: &ManifestClient{ClientID: "browser"}, MachineToMachine: &ManifestClient{ClientID: "service"}}, SecurityEvents: &ManifestSecurityEvents{TransmitterIssuer: "https://auth.example.com/ssf", ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf", Audience: "urn:easyai:ssf:receiver:22222222-2222-2222-2222-222222222222"}, }, MachineCredential: &MachineCredential{ClientID: "service", ClientSecret: "placeholder", IssuedAt: time.Now()}}, } preparer := &securityEventPreparerFake{} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, preparer) pairing, err := service.Start(context.Background(), PairingInput{ AuthCenterURL: "https://auth.example.com", OnboardingCode: "one-time-code-value", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", }, "trace-retry") if err != nil { t.Fatal(err) } if _, err := service.Continue(context.Background(), pairing.ID); err == nil { t.Fatal("simulated pairing status persistence failure was ignored") } firstMachine := repository.revision.MachineCredentialRef firstSession := repository.revision.SessionEncryptionKeyRef if firstMachine == "" || firstSession == "" { t.Fatalf("first delivery was not adopted: %#v", repository.revision) } if _, ok := secrets.values[firstMachine]; !ok { t.Fatal("first adopted machine Secret was deleted") } if _, ok := secrets.values[firstSession]; !ok { t.Fatal("first adopted session Secret was deleted") } for _, reference := range []string{firstMachine, firstSession} { if _, queued := repository.pendingSecretCleanups[reference]; queued { t.Fatalf("ambiguously adopted identity Secret %q was re-queued", reference) } } if _, err := service.Continue(context.Background(), pairing.ID); err == nil { t.Fatal("simulated pairing status persistence failure was ignored") } secondMachine := repository.revision.MachineCredentialRef secondSession := repository.revision.SessionEncryptionKeyRef if secondMachine == firstMachine || secondSession == firstSession { t.Fatalf("identity Secret references were overwritten in place: first=(%s,%s) second=(%s,%s)", firstMachine, firstSession, secondMachine, secondSession) } for _, reference := range []string{firstMachine, firstSession} { if _, queued := repository.pendingSecretCleanups[reference]; !queued { t.Fatalf("first superseded identity Secret %q was not queued for cleanup", reference) } } completed, err := service.Continue(context.Background(), pairing.ID) if err != nil { t.Fatal(err) } if completed.Status != PairingCompleted { t.Fatalf("retry did not complete: %#v", completed) } thirdMachine := repository.revision.MachineCredentialRef thirdSession := repository.revision.SessionEncryptionKeyRef if thirdMachine == secondMachine || thirdSession == secondSession { t.Fatalf("second retry overwrote identity Secret references in place: second=(%s,%s) third=(%s,%s)", secondMachine, secondSession, thirdMachine, thirdSession) } for _, reference := range []string{thirdMachine, thirdSession} { if _, ok := secrets.values[reference]; !ok { t.Fatalf("active identity Secret %q was deleted", reference) } if _, queued := repository.pendingSecretCleanups[reference]; queued { t.Fatalf("active identity Secret %q remained in cleanup queue", reference) } } for _, reference := range []string{firstMachine, firstSession, secondMachine, secondSession} { if _, queued := repository.pendingSecretCleanups[reference]; !queued { t.Fatalf("superseded identity Secret %q was not queued for cleanup", reference) } } } func TestPairingExpirationIsPersistedAndExchangeTokenIsDestroyed(t *testing.T) { repository := &pairingRepositoryFake{exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", ExchangeTokenRef: "identity-exchange-pairing", Status: PairingPreparing, RemoteVersion: 2, ExpiresAt: time.Now().Add(-time.Minute), Version: 4, }} secrets := &secretStoreFake{values: map[string][]byte{"identity-exchange-pairing": []byte("temporary-token")}} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { t.Fatal("expired exchange must not call the remote service") return nil, nil }, nil) expired, err := service.Continue(context.Background(), "pairing") if err != nil { t.Fatal(err) } if expired.Status != PairingExpired || expired.LastErrorCategory != "exchange_expired" { t.Fatalf("expired pairing was not persisted: %#v", expired) } if _, exists := secrets.values["identity-exchange-pairing"]; exists { t.Fatal("expired exchange token remained in SecretStore") } } func TestPairingPersistsOnlySafeSecurityEventFailureCategory(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ ID: "revision", State: RevisionDraft, AuthCenterURL: "https://auth.example.com", SessionRevocation: true, MachineCredentialRef: "identity-machine-revision", }, exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", RemoteExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeTokenRef: "identity-exchange-pairing", Status: PairingCredentialsSaved, RemoteVersion: 4, ExpiresAt: time.Now().Add(time.Hour), Version: 7, }, } secrets := &secretStoreFake{values: map[string][]byte{ "identity-exchange-pairing": []byte("temporary-exchange-token-value-000000"), "identity-machine-revision": []byte("temporary-machine-secret-value-0000000"), }} remote := &onboardingRemoteFake{} preparer := &securityEventPreparerFake{err: safeCategoryTestError{ category: "discovery_failed", message: "sensitive-marker https://internal.example/ssf token-value", }} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, preparer) failed, err := service.Continue(context.Background(), "pairing") if err == nil { t.Fatal("security event discovery failure was ignored") } if failed.Status != PairingCredentialsSaved || failed.LastErrorCategory != "security_event_discovery_failed" { t.Fatalf("unsafe or missing pairing failure state: %#v", failed) } encoded, marshalErr := json.Marshal(failed) if marshalErr != nil { t.Fatal(marshalErr) } if strings.Contains(string(encoded), "sensitive-marker") || strings.Contains(string(encoded), "internal.example") || strings.Contains(string(encoded), "token-value") { t.Fatalf("raw security event error escaped through pairing JSON: %s", encoded) } preparer.err = nil completed, err := service.Continue(context.Background(), "pairing") if err != nil { t.Fatal(err) } if completed.Status != PairingCompleted || completed.LastErrorCategory != "" { t.Fatalf("successful retry did not clear the diagnostic category: %#v", completed) } } func TestPairingCancellationPersistsIntentBeforeCleaningTemporaryResources(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ ID: "revision", State: RevisionDraft, Version: 3, SessionRevocation: true, MachineCredentialRef: "identity-machine-revision", SessionEncryptionKeyRef: "identity-session-revision", }, exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", ExchangeTokenRef: "identity-exchange-pairing", Status: PairingCredentialsSaved, CleanupStatus: PairingCleanupNone, RemoteVersion: 4, ExpiresAt: time.Now().Add(time.Hour), Version: 7, }, } secrets := &secretStoreFake{values: map[string][]byte{ "identity-exchange-pairing": []byte("temporary-exchange-token-value-000000"), "identity-machine-revision": []byte("temporary-machine-secret-value-0000000"), "identity-session-revision": []byte("temporary-session-secret-value-0000000"), }} preparer := &securityEventPreparerFake{} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { t.Fatal("cancellation must not call the onboarding remote") return nil, nil }, preparer) cancelled, err := service.Cancel(context.Background(), "pairing", 7, "trace-cancel", "audit-cancel") if err != nil { t.Fatal(err) } if cancelled.Status != PairingCancelled || cancelled.CleanupStatus != PairingCleanupPending || cancelled.CancelledAt == nil { t.Fatalf("cancellation intent was not persisted: %#v", cancelled) } if repository.revision.State != RevisionFailed || repository.revision.LastErrorCategory != "pairing_cancelled" { t.Fatalf("draft was not atomically retired: %#v", repository.revision) } if len(secrets.values) != 3 { t.Fatal("synchronous cancellation removed resources before the cleanup worker could resume them") } replayed, err := service.Cancel(context.Background(), "pairing", 7, "trace-replay", "audit-replay") if err != nil || replayed.Version != cancelled.Version || replayed.Status != PairingCancelled { t.Fatalf("lost cancellation response could not be replayed safely: %#v err=%v", replayed, err) } cleaned, err := service.Cleanup(context.Background(), "pairing") if err != nil { t.Fatal(err) } if cleaned.CleanupStatus != PairingCleanupCompleted || cleaned.CleanupCompletedAt == nil || !preparer.cleanupCalled { t.Fatalf("pairing cleanup was not completed: %#v", cleaned) } if len(secrets.values) != 0 || repository.revision.MachineCredentialRef != "" || repository.revision.SessionEncryptionKeyRef != "" { t.Fatalf("temporary secret references survived cleanup: secrets=%d revision=%#v", len(secrets.values), repository.revision) } } func TestPairingCleanupRetriesWithSafeCategory(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ID: "revision", State: RevisionFailed, Version: 4, SessionRevocation: true}, exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", Status: PairingCancelled, CleanupStatus: PairingCleanupPending, RemoteVersion: 4, ExpiresAt: time.Now().Add(time.Hour), Version: 8, }, } preparer := &securityEventPreparerFake{cleanupErr: safeCategoryTestError{ category: "retirement_pending", message: "sensitive cleanup marker token-value", }} service := NewPairingService(repository, &secretStoreFake{values: map[string][]byte{}}, nil, preparer) pending, err := service.Cleanup(context.Background(), "pairing") if err == nil { t.Fatal("pending security event retirement was treated as completed") } if pending.CleanupStatus != PairingCleanupPending || pending.LastErrorCategory != "cleanup_security_event_retirement_pending" || strings.Contains(pending.LastErrorCategory, "sensitive") { t.Fatalf("cleanup failure was not safely persisted: %#v", pending) } preparer.cleanupErr = nil completed, err := service.Cleanup(context.Background(), "pairing") if err != nil { t.Fatal(err) } if completed.CleanupStatus != PairingCleanupCompleted { t.Fatalf("cleanup retry did not complete: %#v", completed) } } func TestPairingMapsUntrustedRemoteErrorCategoryToStableValue(t *testing.T) { repository := &pairingRepositoryFake{exchange: PairingExchange{ID: "pairing", Version: 2}} service := NewPairingService(repository, &secretStoreFake{}, nil, nil) updated, err := service.updateFromRemote(context.Background(), repository.exchange, Exchange{ Status: ExchangeFailed, Version: 3, LastErrorCategory: "secret_token_abcdef", }) if err != nil { t.Fatal(err) } if updated.Status != PairingFailed || updated.LastErrorCategory != "remote_exchange_failed" { t.Fatalf("untrusted remote category was persisted: %#v", updated) } } func TestPairingConflictRetirementIsScopedToCurrentBlockedPairing(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ID: "revision", State: RevisionDraft, Version: 3, SessionRevocation: true}, exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", Status: PairingCredentialsSaved, CleanupStatus: PairingCleanupNone, Version: 7, LastErrorCategory: "security_event_connection_conflict", }, } preparer := &securityEventPreparerFake{} service := NewPairingService(repository, &secretStoreFake{}, nil, preparer) resolved, err := service.RetireConflictingSecurityEvents(context.Background(), "pairing", 7) if err != nil { t.Fatal(err) } if resolved.ID != "pairing" || resolved.LastErrorCategory != "security_event_retirement_pending" || !preparer.conflictRetireCalled || preparer.conflictRevisionID != "revision" { t.Fatalf("conflict retirement escaped pairing scope: pairing=%#v preparer=%#v", resolved, preparer) } preparer.conflictRetireCalled = false if _, err := service.RetireConflictingSecurityEvents(context.Background(), "pairing", 6); !errors.Is(err, ErrRevisionConflict) { t.Fatalf("stale pairing version error=%v", err) } repository.exchange.LastErrorCategory = "" if _, err := service.RetireConflictingSecurityEvents(context.Background(), "pairing", 7); !errors.Is(err, ErrPairingConflictNotResolvable) { t.Fatalf("resolved pairing could retire another connection: %v", err) } if preparer.conflictRetireCalled { t.Fatal("stale conflict action reached the security event manager") } } func TestCompletedPairingRestoresPreparedReceiverAfterRestart(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ ID: "revision", State: RevisionValidated, SessionRevocation: true, MachineCredentialRef: "identity-machine-revision", }, exchange: PairingExchange{ID: "pairing", RevisionID: "revision", Status: PairingCompleted, Version: 8}, } secrets := &secretStoreFake{values: map[string][]byte{ "identity-machine-revision": []byte("machine-secret-value-restored-from-store"), }} preparer := &securityEventPreparerFake{} service := NewPairingService(repository, secrets, nil, preparer) if err := service.RestoreCompletedSecurityEvents(context.Background(), "pairing"); err != nil { t.Fatal(err) } if !preparer.called { t.Fatal("completed pairing did not reconstruct its prepared security event receiver") } } func TestCompletedPairingDoesNotRestoreReceiverForSupersededRevision(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ ID: "revision", State: RevisionSuperseded, Version: 5, SessionRevocation: true, MachineCredentialRef: "identity-machine-revision", }, exchange: PairingExchange{ID: "pairing", RevisionID: "revision", Status: PairingCompleted, Version: 8}, } preparer := &securityEventPreparerFake{} service := NewPairingService(repository, &secretStoreFake{values: map[string][]byte{ "identity-machine-revision": []byte("must-not-be-read"), }}, nil, preparer) if err := service.RestoreCompletedSecurityEvents(context.Background(), "pairing"); !errors.Is(err, ErrRevisionConflict) { t.Fatalf("restore error=%v, want revision conflict", err) } if preparer.called { t.Fatal("superseded Revision reconstructed an orphan prepared Receiver") } } func TestCancelledPairingCleanupWaitsForCompletedReceiverRestore(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ ID: "revision", State: RevisionValidated, Version: 3, SessionRevocation: true, MachineCredentialRef: "identity-machine-revision", }, exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", ExchangeTokenRef: "identity-exchange-pairing", Status: PairingCompleted, CleanupStatus: PairingCleanupNone, Version: 8, }, } secrets := &secretStoreFake{values: map[string][]byte{ "identity-exchange-pairing": []byte("exchange-token"), "identity-machine-revision": []byte("machine-secret"), }} preparer := &blockingSecurityEventPreparer{ prepareStarted: make(chan struct{}), releasePrepare: make(chan struct{}), cleanupStarted: make(chan struct{}), } service := NewPairingService(repository, secrets, nil, preparer) restoreResult := make(chan error, 1) go func() { restoreResult <- service.RestoreCompletedSecurityEvents(context.Background(), "pairing") }() <-preparer.prepareStarted cancelled, err := service.Cancel(context.Background(), "pairing", 8, "trace", "audit") if err != nil { t.Fatal(err) } cleanupResult := make(chan PairingExchange, 1) cleanupError := make(chan error, 1) go func() { cleaned, cleanupErr := service.Cleanup(context.Background(), "pairing") cleanupResult <- cleaned cleanupError <- cleanupErr }() select { case <-preparer.cleanupStarted: t.Fatal("cleanup overtook an in-flight completed receiver restore") case <-time.After(100 * time.Millisecond): } close(preparer.releasePrepare) if err := <-restoreResult; !errors.Is(err, ErrRevisionConflict) { t.Fatalf("restore error = %v, want revision conflict after cancellation", err) } if err := <-cleanupError; err != nil { t.Fatal(err) } cleaned := <-cleanupResult if cancelled.Status != PairingCancelled || cleaned.CleanupStatus != PairingCleanupCompleted { t.Fatalf("cancel/cleanup did not converge: cancelled=%#v cleaned=%#v", cancelled, cleaned) } if len(secrets.values) != 0 { t.Fatalf("cancelled restore left temporary secrets: %d", len(secrets.values)) } } func TestPairingClassifiesExchangeCompletionFailureSeparatelyFromSSF(t *testing.T) { repository := &pairingRepositoryFake{ revision: Revision{ID: "revision", State: RevisionDraft, AuthCenterURL: "https://auth.example.com"}, exchange: PairingExchange{ ID: "pairing", RevisionID: "revision", RemoteExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeTokenRef: "identity-exchange-pairing", Status: PairingCredentialsSaved, RemoteVersion: 4, ExpiresAt: time.Now().Add(time.Hour), Version: 7, }, } secrets := &secretStoreFake{values: map[string][]byte{ "identity-exchange-pairing": []byte("temporary-exchange-token-value-000000"), }} remote := &onboardingRemoteFake{completeErr: errors.New("upstream body contains sensitive marker")} service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, nil) failed, err := service.Continue(context.Background(), "pairing") if err == nil { t.Fatal("exchange completion failure was ignored") } if failed.LastErrorCategory != "exchange_completion_failed" { t.Fatalf("completion failure was misclassified: %#v", failed) } }