Files
steipete--codexbar/Tests/CodexBarTests/ClaudeOAuthHistoryCredentialRoutingTests.swift
2026-07-13 12:22:33 +08:00

170 lines
8.2 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
@Suite(.serialized)
struct ClaudeOAuthHistoryCredentialRoutingTests {
@Test
func `history keychain reference only matches the credential that won routing`() throws {
let keychainData = self.makeCredentialsData(accessToken: "keychain-token")
let keychainCredentials = try ClaudeOAuthCredentials.parse(data: keychainData)
let differentCredentials = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "different-token"))
let fingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
modifiedAt: 1,
createdAt: 1,
persistentRefHash: "opaque-ref")
let matchingCLIRecord = ClaudeOAuthCredentialRecord(
credentials: keychainCredentials,
owner: .claudeCLI,
source: .memoryCache)
let differentCLIRecord = ClaudeOAuthCredentialRecord(
credentials: differentCredentials,
owner: .claudeCLI,
source: .credentialsFile)
let matchingEnvironmentRecord = ClaudeOAuthCredentialRecord(
credentials: keychainCredentials,
owner: .environment,
source: .environment)
let matchingCodexBarRecord = ClaudeOAuthCredentialRecord(
credentials: keychainCredentials,
owner: .codexbar,
source: .cacheKeychain)
ProviderInteractionContext.$current.withValue(.userInitiated) {
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: keychainData,
fingerprint: fingerprint)
{
#expect(ClaudeOAuthCredentialsStore
.matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: matchingCLIRecord) == "opaque-ref")
#expect(ClaudeOAuthCredentialsStore
.matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: differentCLIRecord) == nil)
#expect(ClaudeOAuthCredentialsStore
.matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: matchingEnvironmentRecord) == nil)
#expect(ClaudeOAuthCredentialsStore
.matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: matchingCodexBarRecord) == nil)
#expect(ClaudeOAuthCredentialsStore
.claudeKeychainCredentialMatchWithoutPrompt(for: matchingCLIRecord) ==
.matched(persistentRefHash: "opaque-ref"))
#expect(ClaudeOAuthCredentialsStore
.claudeKeychainCredentialMatchWithoutPrompt(for: differentCLIRecord) == .mismatch)
#expect(ClaudeOAuthCredentialsStore
.claudeKeychainCredentialMatchWithoutPrompt(for: matchingEnvironmentRecord) == .notApplicable)
}
}
}
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: nil,
fingerprint: fingerprint)
{
let unavailable = ClaudeOAuthCredentialsStore
.claudeKeychainCredentialMatchWithoutPrompt(for: matchingCLIRecord)
#expect(unavailable == .unavailable)
#expect(unavailable.isUnavailable)
#expect(!unavailable.isMismatch)
}
let absentStore = ClaudeOAuthCredentialsStore.ClaudeKeychainOverrideStore()
ClaudeOAuthCredentialsStore.withMutableClaudeKeychainOverrideStoreForTesting(absentStore) {
#expect(ClaudeOAuthCredentialsStore
.claudeKeychainCredentialMatchWithoutPrompt(for: matchingCLIRecord) == .absent)
}
}
@Test
func `newest duplicate reference cannot label a different winning credential`() throws {
let winningCredentials = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "winning-token"))
let newestCandidateCredentials = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "newest-candidate-token"))
let winningRecord = ClaudeOAuthCredentialRecord(
credentials: winningCredentials,
owner: .claudeCLI,
source: .memoryCache)
let newestCandidateRecord = ClaudeOAuthCredentialRecord(
credentials: newestCandidateCredentials,
owner: .claudeCLI,
source: .claudeKeychain)
#expect(ClaudeOAuthCredentialsStore._matchingClaudeKeychainPersistentRefHashForTesting(
record: winningRecord,
candidateCredentials: newestCandidateCredentials,
persistentRefHash: "newest-candidate-ref") == nil)
#expect(ClaudeOAuthCredentialsStore._matchingClaudeKeychainPersistentRefHashForTesting(
record: newestCandidateRecord,
candidateCredentials: newestCandidateCredentials,
persistentRefHash: "newest-candidate-ref") == "newest-candidate-ref")
}
@Test
func `history owner follows refresh credential across access token rotation`() throws {
let beforeRefresh = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "access-before", refreshToken: "stable-refresh"))
let afterRefresh = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "access-after", refreshToken: "stable-refresh"))
let beforeIdentifier = try #require(beforeRefresh.historyOwnerIdentifier)
let afterIdentifier = try #require(afterRefresh.historyOwnerIdentifier)
#expect(beforeIdentifier == afterIdentifier)
#expect(beforeIdentifier.count == 64)
#expect(!beforeIdentifier.contains("stable-refresh"))
}
@Test
func `access-only credential replacement rotates history owner`() throws {
let original = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "original-access"))
let replacement = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "replacement-access"))
let originalIdentifier = try #require(original.historyOwnerIdentifier)
let replacementIdentifier = try #require(replacement.historyOwnerIdentifier)
#expect(originalIdentifier != replacementIdentifier)
#expect(!originalIdentifier.contains("original-access"))
#expect(!replacementIdentifier.contains("replacement-access"))
}
@Test
func `only an explicit refresh lineage can preserve a rotated credential owner`() throws {
let original = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "access-before", refreshToken: "refresh-before"))
let rotated = try ClaudeOAuthCredentials.parse(
data: self.makeCredentialsData(accessToken: "access-after", refreshToken: "refresh-after"))
let originalIdentifier = try #require(original.historyOwnerIdentifier)
let rotatedIdentifier = try #require(rotated.historyOwnerIdentifier)
#expect(originalIdentifier != rotatedIdentifier)
let refreshProvenRecord = ClaudeOAuthCredentialRecord(
credentials: rotated,
owner: .codexbar,
source: .memoryCache,
historyOwnerIdentifier: originalIdentifier)
let unrelatedReplacementRecord = ClaudeOAuthCredentialRecord(
credentials: rotated,
owner: .codexbar,
source: .cacheKeychain)
#expect(refreshProvenRecord.historyOwnerIdentifier == originalIdentifier)
#expect(unrelatedReplacementRecord.historyOwnerIdentifier == rotatedIdentifier)
}
private func makeCredentialsData(accessToken: String, refreshToken: String? = nil) -> Data {
let expiresAt = Int(Date(timeIntervalSinceNow: 3600).timeIntervalSince1970 * 1000)
let refreshTokenJSON = refreshToken.map { "\n \"refreshToken\": \"\($0)\"," } ?? ""
return Data("""
{
"claudeAiOauth": {
"accessToken": "\(accessToken)",
\(refreshTokenJSON)
"expiresAt": \(expiresAt),
"scopes": ["user:profile"]
}
}
""".utf8)
}
}