create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'provider' => 'microsoft', ]); $secret = 'spec081-secret-created'; $this->actingAs($user); app(CredentialManager::class)->upsertClientSecretCredential( connection: $connection, clientId: 'spec081-client-created', clientSecret: $secret, ); $log = AuditLog::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('action', 'provider_connection.credentials_created') ->latest('id') ->first(); expect($log)->not->toBeNull() ->and($log?->resource_type)->toBe('provider_connection') ->and($log?->resource_id)->toBe((string) $connection->getKey()) ->and($log?->actor_id)->toBe((int) $user->getKey()) ->and($log?->metadata['provider_connection_id'] ?? null)->toBe((int) $connection->getKey()) ->and($log?->metadata['changed_fields'] ?? [])->toContain('client_id') ->and($log?->metadata['changed_fields'] ?? [])->toContain('client_secret') ->and($log?->metadata['redacted_fields'] ?? [])->toContain('client_secret') ->and((string) json_encode($log?->metadata ?? []))->not->toContain($secret); }); it('Spec081 audits client id updates as credentials_updated without leaking secrets', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'provider' => 'microsoft', ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'spec081-client-before', 'client_secret' => 'spec081-secret-before', ], ]); $this->actingAs($user); app(CredentialManager::class)->updateClientIdPreservingSecret( connection: $connection, clientId: 'spec081-client-after', ); $log = AuditLog::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('action', 'provider_connection.credentials_updated') ->latest('id') ->first(); expect($log)->not->toBeNull() ->and($log?->resource_type)->toBe('provider_connection') ->and($log?->resource_id)->toBe((string) $connection->getKey()) ->and($log?->metadata['changed_fields'] ?? [])->toContain('client_id') ->and($log?->metadata['changed_fields'] ?? [])->not->toContain('client_secret') ->and((string) json_encode($log?->metadata ?? []))->not->toContain('spec081-secret-before'); }); it('Spec081 audits secret rotation as credentials_rotated with redacted metadata', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'provider' => 'microsoft', ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'spec081-client-stable', 'client_secret' => 'spec081-secret-before-rotate', ], ]); $this->actingAs($user); $rotatedSecret = 'spec081-secret-after-rotate'; app(CredentialManager::class)->upsertClientSecretCredential( connection: $connection, clientId: 'spec081-client-stable', clientSecret: $rotatedSecret, ); $log = AuditLog::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('action', 'provider_connection.credentials_rotated') ->latest('id') ->first(); expect($log)->not->toBeNull() ->and($log?->resource_type)->toBe('provider_connection') ->and($log?->resource_id)->toBe((string) $connection->getKey()) ->and($log?->metadata['changed_fields'] ?? [])->toContain('client_secret') ->and($log?->metadata['redacted_fields'] ?? [])->toContain('client_secret') ->and((string) json_encode($log?->metadata ?? []))->not->toContain($rotatedSecret); });