369 lines · 15.9 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore #if os(macOS)@Suite(.serialized)struct ClaudeOAuthFetchStrategyAvailabilityTests {    private struct StubClaudeFetcher: ClaudeUsageFetching {        func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {            throw ClaudeUsageError.parseFailed("stub")        }         func debugRawProbe(model _: String) async -> String {            "stub"        }         func detectVersion() -> String? {            nil        }    }     private func makeContext(        sourceMode: ProviderSourceMode,        env: [String: String] = [:]) -> ProviderFetchContext    {        ProviderFetchContext(            runtime: .app,            sourceMode: sourceMode,            includeCredits: false,            webTimeout: 1,            webDebugDumpHTML: false,            verbose: false,            env: env,            settings: nil,            fetcher: UsageFetcher(environment: env),            claudeFetcher: StubClaudeFetcher(),            browserDetection: BrowserDetection(cacheTTL: 0))    }     private func expiredRecord(owner: ClaudeOAuthCredentialOwner = .claudeCLI) -> ClaudeOAuthCredentialRecord {        ClaudeOAuthCredentialRecord(            credentials: ClaudeOAuthCredentials(                accessToken: "expired-token",                refreshToken: "refresh-token",                expiresAt: Date(timeIntervalSinceNow: -60),                scopes: ["user:profile"],                rateLimitTier: nil),            owner: owner,            source: .cacheKeychain)    }     @Test    func `auto mode expired creds cli available returns available`() async {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let available = await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride            .withValue(self.expiredRecord()) {                await ClaudeOAuthFetchStrategy.$claudeCLIAvailableOverride.withValue(true) {                    await strategy.isAvailable(context)                }            }        #expect(available == true)    }     @Test    func `auto mode expired creds cli unavailable returns unavailable`() async {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let available = await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride            .withValue(self.expiredRecord()) {                await ClaudeOAuthFetchStrategy.$claudeCLIAvailableOverride.withValue(false) {                    await strategy.isAvailable(context)                }            }        #expect(available == false)    }     @Test    func `oauth mode expired creds cli available returns available`() async {        let context = self.makeContext(sourceMode: .oauth)        let strategy = ClaudeOAuthFetchStrategy()        let available = await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride            .withValue(self.expiredRecord()) {                await ClaudeOAuthFetchStrategy.$claudeCLIAvailableOverride.withValue(true) {                    await strategy.isAvailable(context)                }            }        #expect(available == true)    }     @Test    func `auto mode expired codexbar creds cli unavailable still available`() async {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let available = await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride            .withValue(self.expiredRecord(owner: .codexbar)) {                await ClaudeOAuthFetchStrategy.$claudeCLIAvailableOverride.withValue(false) {                    await strategy.isAvailable(context)                }            }        #expect(available == true)    }     @Test    func `oauth mode does not fallback after O auth failure`() {        let context = self.makeContext(sourceMode: .oauth)        let strategy = ClaudeOAuthFetchStrategy()        #expect(strategy.shouldFallback(            on: ClaudeUsageError.oauthFailed("oauth failed"),            context: context) == false)    }     @Test    func `auto mode falls back after O auth failure`() {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        #expect(strategy.shouldFallback(            on: ClaudeUsageError.oauthFailed("oauth failed"),            context: context) == true)    }     @Test    func `auto mode user initiated clears keychain cooldown gate`() async {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let recordWithoutRequiredScope = ClaudeOAuthCredentialRecord(            credentials: ClaudeOAuthCredentials(                accessToken: "expired-token",                refreshToken: "refresh-token",                expiresAt: Date(timeIntervalSinceNow: -60),                scopes: ["user:inference"],                rateLimitTier: nil),            owner: .claudeCLI,            source: .cacheKeychain)         await KeychainAccessGate.withTaskOverrideForTesting(false) {            ClaudeOAuthKeychainAccessGate.resetForTesting()            defer { ClaudeOAuthKeychainAccessGate.resetForTesting() }             let now = Date(timeIntervalSince1970: 1000)            ClaudeOAuthKeychainAccessGate.recordDenied(now: now)            #expect(ClaudeOAuthKeychainAccessGate.shouldAllowPrompt(now: now) == false)             _ = await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride                .withValue(recordWithoutRequiredScope) {                    await ProviderInteractionContext.$current.withValue(.userInitiated) {                        await strategy.isAvailable(context)                    }                }             #expect(ClaudeOAuthKeychainAccessGate.shouldAllowPrompt(now: now))        }    }     @Test    func `auto mode only on user action background startup without cache is available for bootstrap`() async throws {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"         try await KeychainCacheStore.withServiceOverrideForTesting(service) {            KeychainCacheStore.setTestStoreForTesting(true)            defer { KeychainCacheStore.setTestStoreForTesting(false) }             try await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                ClaudeOAuthCredentialsStore.invalidateCache()                ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()                ClaudeOAuthKeychainAccessGate.resetForTesting()                defer {                    ClaudeOAuthCredentialsStore.invalidateCache()                    ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()                    ClaudeOAuthKeychainAccessGate.resetForTesting()                }                 let tempDir = FileManager.default.temporaryDirectory                    .appendingPathComponent(UUID().uuidString, isDirectory: true)                try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)                let fileURL = tempDir.appendingPathComponent("credentials.json")                 let available = await KeychainAccessGate.withTaskOverrideForTesting(false) {                    await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) {                        await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(.securityFramework) {                            await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {                                await ProviderRefreshContext.$current.withValue(.startup) {                                    await ProviderInteractionContext.$current.withValue(.background) {                                        await strategy.isAvailable(context)                                    }                                }                            }                        }                    }                }                 #expect(available == true)            }        }    }     @Test    func `auto mode expired Claude CLI creds env provided CLI override returns available`() async throws {        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let cliURL = tempDir.appendingPathComponent("claude")        try Data("#!/bin/sh\nexit 0\n".utf8).write(to: cliURL)        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: cliURL.path)         let context = self.makeContext(            sourceMode: .auto,            env: ["CLAUDE_CLI_PATH": cliURL.path])        let strategy = ClaudeOAuthFetchStrategy()        let available = await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride            .withValue(self.expiredRecord()) {                await strategy.isAvailable(context)            }         #expect(available == true)    }     @Test    func `auto mode default reader keeps background startup bootstrap available`() async throws {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"         try await KeychainCacheStore.withServiceOverrideForTesting(service) {            KeychainCacheStore.setTestStoreForTesting(true)            defer { KeychainCacheStore.setTestStoreForTesting(false) }             try await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                ClaudeOAuthCredentialsStore.invalidateCache()                ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()                ClaudeOAuthKeychainAccessGate.resetForTesting()                defer {                    ClaudeOAuthCredentialsStore.invalidateCache()                    ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()                    ClaudeOAuthKeychainAccessGate.resetForTesting()                }                 let tempDir = FileManager.default.temporaryDirectory                    .appendingPathComponent(UUID().uuidString, isDirectory: true)                try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)                let fileURL = tempDir.appendingPathComponent("credentials.json")                 let available = await KeychainAccessGate.withTaskOverrideForTesting(false) {                    await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) {                        await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {                            await ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(.nonZeroExit) {                                await ProviderRefreshContext.$current.withValue(.startup) {                                    await ProviderInteractionContext.$current.withValue(.background) {                                        await strategy.isAvailable(context)                                    }                                }                            }                        }                    }                }                 #expect(available == true)            }        }    }     @Test    func `auto mode experimental reader ignores prompt policy cooldown gate`() async {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let securityData = Data("""        {          "claudeAiOauth": {            "accessToken": "security-token",            "expiresAt": \(Int(Date(timeIntervalSinceNow: 3600).timeIntervalSince1970 * 1000)),            "scopes": ["user:profile"]          }        }        """.utf8)         let recordWithoutRequiredScope = ClaudeOAuthCredentialRecord(            credentials: ClaudeOAuthCredentials(                accessToken: "token-no-scope",                refreshToken: "refresh-token",                expiresAt: Date(timeIntervalSinceNow: -60),                scopes: ["user:inference"],                rateLimitTier: nil),            owner: .claudeCLI,            source: .cacheKeychain)         let available = await KeychainAccessGate.withTaskOverrideForTesting(false) {            await ClaudeOAuthKeychainAccessGate.withShouldAllowPromptOverrideForTesting(false) {                await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(                    .securityCLIExperimental)                {                    await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {                        await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride.withValue(                            recordWithoutRequiredScope)                        {                            await ProviderInteractionContext.$current.withValue(.background) {                                await ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(.data(                                    securityData))                                {                                    await strategy.isAvailable(context)                                }                            }                        }                    }                }            }        }         #expect(available == true)    }     @Test    func `auto mode experimental reader security failure blocks availability when stored policy blocks fallback`()        async    {        let context = self.makeContext(sourceMode: .auto)        let strategy = ClaudeOAuthFetchStrategy()        let fallbackData = Data("""        {          "claudeAiOauth": {            "accessToken": "fallback-token",            "expiresAt": \(Int(Date(timeIntervalSinceNow: 3600).timeIntervalSince1970 * 1000)),            "scopes": ["user:profile"]          }        }        """.utf8)         let recordWithoutRequiredScope = ClaudeOAuthCredentialRecord(            credentials: ClaudeOAuthCredentials(                accessToken: "token-no-scope",                refreshToken: "refresh-token",                expiresAt: Date(timeIntervalSinceNow: -60),                scopes: ["user:inference"],                rateLimitTier: nil),            owner: .claudeCLI,            source: .cacheKeychain)         let available = await KeychainAccessGate.withTaskOverrideForTesting(false) {            await ClaudeOAuthKeychainAccessGate.withShouldAllowPromptOverrideForTesting(true) {                await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(                    .securityCLIExperimental)                {                    await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {                        await ClaudeOAuthFetchStrategy.$nonInteractiveCredentialRecordOverride.withValue(                            recordWithoutRequiredScope)                        {                            await ProviderInteractionContext.$current.withValue(.background) {                                await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(                                    data: fallbackData,                                    fingerprint: nil)                                {                                    await ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(                                        .nonZeroExit)                                    {                                        await strategy.isAvailable(context)                                    }                                }                            }                        }                    }                }            }        }         #expect(available == false)    }}#endif