import Foundation import Testing @testable import CodexBarCore @Suite(.serialized) struct PerplexityCookieCacheTests { private static let testToken = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.fake-test-token" private static let testCookieName = PerplexityCookieHeader.defaultSessionCookieName 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 } } // MARK: - Cache round-trip @Test func `cache round trip produces valid cookie override`() { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=\(Self.testToken)", sourceLabel: "web") let cached = CookieHeaderCache.load(provider: .perplexity) #expect(cached != nil) #expect(cached?.sourceLabel == "web") let override = PerplexityCookieHeader.override(from: cached?.cookieHeader) #expect(override?.name == Self.testCookieName) #expect(override?.token == Self.testToken) } // MARK: - isAvailable returns true when cache has entry @Test func `is available returns true when cache populated`() { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } // With no cache and no other sources, load should return nil let beforeStore = CookieHeaderCache.load(provider: .perplexity) #expect(beforeStore == nil) // After storing, cache should be available CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=\(Self.testToken)", sourceLabel: "web") let afterStore = CookieHeaderCache.load(provider: .perplexity) #expect(afterStore != nil) } // MARK: - Cache cleared on invalidToken @Test func `cache cleared on invalid token`() { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=\(Self.testToken)", sourceLabel: "web") // Verify it's cached #expect(CookieHeaderCache.load(provider: .perplexity) != nil) // Simulate what fetch() does on invalidToken: clear the cache CookieHeaderCache.clear(provider: .perplexity) #expect(CookieHeaderCache.load(provider: .perplexity) == nil) } // MARK: - Cache NOT cleared on non-auth errors @Test func `cache not cleared on network error`() { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=\(Self.testToken)", sourceLabel: "web") // Simulate a networkError — cache should NOT be cleared let error = PerplexityAPIError.networkError("timeout") switch error { case .invalidToken: CookieHeaderCache.clear(provider: .perplexity) default: break // non-auth errors do not clear cache } #expect(CookieHeaderCache.load(provider: .perplexity) != nil) } @Test func `cache not cleared on API error`() { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=\(Self.testToken)", sourceLabel: "web") // Simulate an apiError (e.g. HTTP 500) — cache should NOT be cleared let error = PerplexityAPIError.apiError("HTTP 500") switch error { case .invalidToken: CookieHeaderCache.clear(provider: .perplexity) default: break // non-auth errors do not clear cache } #expect(CookieHeaderCache.load(provider: .perplexity) != nil) } // MARK: - Bare token stored as default cookie name @Test func `bare token round trips with default cookie name`() { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } // Store with default cookie name format CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=\(Self.testToken)", sourceLabel: "web") let cached = CookieHeaderCache.load(provider: .perplexity) let override = PerplexityCookieHeader.override(from: cached?.cookieHeader) #expect(override?.name == Self.testCookieName) #expect(override?.token == Self.testToken) } @Test func `off mode ignores cached session cookie`() async { KeychainCacheStore.setTestStoreForTesting(true) defer { CookieHeaderCache.clear(provider: .perplexity) KeychainCacheStore.setTestStoreForTesting(false) } CookieHeaderCache.store( provider: .perplexity, cookieHeader: "\(Self.testCookieName)=cached-token", sourceLabel: "web") let strategy = PerplexityWebFetchStrategy() let settings = ProviderSettingsSnapshot.make( perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings( cookieSource: .off, manualCookieHeader: nil)) let context = ProviderFetchContext( runtime: .app, sourceMode: .auto, includeCredits: false, webTimeout: 1, webDebugDumpHTML: false, verbose: false, env: [:], settings: settings, fetcher: UsageFetcher(environment: [:]), claudeFetcher: StubClaudeFetcher(), browserDetection: BrowserDetection(cacheTTL: 0)) #expect(await strategy.isAvailable(context) == false) } }