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

376 lines
13 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
private final class CookieCallbackHarness: @unchecked Sendable {
private let lock = NSLock()
private var callback: (@Sendable () -> Void)?
func capture(_ callback: @escaping @Sendable () -> Void) {
self.lock.withLock { self.callback = callback }
}
func finish() {
let callback = self.lock.withLock {
let callback = self.callback
self.callback = nil
return callback
}
callback?()
}
}
private final class CookieCallbackFlag: @unchecked Sendable {
private let lock = NSLock()
private var storedValue = false
var value: Bool {
self.lock.withLock { self.storedValue }
}
func set() {
self.lock.withLock { self.storedValue = true }
}
}
private final class CookieOperationLog: @unchecked Sendable {
private let lock = NSLock()
private var entries: [String] = []
var snapshot: [String] {
self.lock.withLock { self.entries }
}
func append(_ entry: String) {
self.lock.withLock { self.entries.append(entry) }
}
}
private final class CookieTimeoutProbe: @unchecked Sendable {
private let lock = NSLock()
private var storedFiredAt: Date?
var firedAt: Date? {
self.lock.withLock { self.storedFiredAt }
}
func record() {
self.lock.withLock {
if self.storedFiredAt == nil {
self.storedFiredAt = Date()
}
}
}
}
struct OpenAIDashboardBrowserCookieImporterTests {
@Test
func `profile denial names exact running component`() {
let hint = OpenAIDashboardBrowserCookieImporter.browserProfileAccessHint(
for: .chrome,
issue: .accessDenied,
processName: "CodexBarCLI",
executablePath: "/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
#expect(hint.contains("macOS denied Chrome profile access"))
#expect(hint.contains("CodexBarCLI (/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI)"))
#expect(hint.contains("Full Disk Access"))
}
@Test
func `profile denial names app bundle for menu refresh`() {
let hint = OpenAIDashboardBrowserCookieImporter.browserProfileAccessHint(
for: .chrome,
issue: .accessDenied,
processName: "CodexBar",
executablePath: "/Applications/CodexBar.app/Contents/MacOS/CodexBar")
#expect(hint.contains("CodexBar.app (/Applications/CodexBar.app)"))
}
@Test
func `browser cookie timeout remains distinct from permission denial`() {
let error = OpenAIDashboardBrowserCookieImporter.browserCookieLoadTimeoutError(
for: .chrome,
processName: "CodexBarCLI",
executablePath: "/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
if case .browserCookieLoadTimedOut = error {
// Expected: a shared deadline does not prove macOS denied access.
} else {
Issue.record("Expected browser cookie load timeout")
}
#expect(error.localizedDescription.contains("Chrome did not finish before the web timeout"))
#expect(!error.localizedDescription.contains("access denied"))
#expect(error.localizedDescription.contains("CodexBarCLI"))
#expect(error.localizedDescription.contains("Keychain prompt"))
#expect(error.localizedDescription.contains("Full Disk Access"))
}
@Test
func `shared deadline clamps each local timeout to remaining budget`() throws {
let start = Date(timeIntervalSinceReferenceDate: 1000)
let deadline = start.addingTimeInterval(30)
let remaining = try OpenAIDashboardBrowserCookieImporter.remainingTimeout(
until: deadline,
cappedAt: 10,
now: start.addingTimeInterval(27))
#expect(remaining == 3)
}
@Test
func `shared deadline preserves smaller local timeout`() throws {
let start = Date(timeIntervalSinceReferenceDate: 1000)
let deadline = start.addingTimeInterval(30)
let remaining = try OpenAIDashboardBrowserCookieImporter.remainingTimeout(
until: deadline,
cappedAt: 10,
now: start.addingTimeInterval(5))
#expect(remaining == 10)
}
@Test
func `expired shared deadline throws structured timeout`() {
let deadline = Date(timeIntervalSinceReferenceDate: 1000)
do {
_ = try OpenAIDashboardBrowserCookieImporter.remainingTimeout(
until: deadline,
now: deadline)
Issue.record("Expected deadline timeout")
} catch let error as URLError {
#expect(error.code == .timedOut)
} catch {
Issue.record("Unexpected error: \(error)")
}
}
@Test
func `blocking browser cookie load cannot exceed shared deadline`() async throws {
let start = Date()
let timeoutProbe = CookieTimeoutProbe()
do {
_ = try await OpenAIDashboardBrowserCookieImporter.runBoundedCookieLoad(
deadline: start.addingTimeInterval(0.05),
timeoutObserver: timeoutProbe.record)
{
Thread.sleep(forTimeInterval: 0.5)
return true
}
Issue.record("Expected cookie load timeout")
} catch let error as URLError {
#expect(error.code == .timedOut)
} catch {
Issue.record("Unexpected error: \(error)")
}
let firedAt = try #require(timeoutProbe.firedAt)
#expect(firedAt.timeIntervalSince(start) < 0.3)
}
@Test
func `timeout observer stays silent when operation wins`() async throws {
let timeoutProbe = CookieTimeoutProbe()
let value = try await OpenAIDashboardBrowserCookieImporter.runBoundedCookieLoad(
deadline: Date().addingTimeInterval(0.05),
timeoutObserver: timeoutProbe.record)
{
true
}
try await Task.sleep(for: .milliseconds(100))
#expect(value)
#expect(timeoutProbe.firedAt == nil)
}
@Test
func `bounded cookie loads preserve explicit retry context`() async throws {
BrowserCookieAccessGate.resetForTesting()
defer { BrowserCookieAccessGate.resetForTesting() }
let start = Date()
for deadline in [nil, Date().addingTimeInterval(1)] {
BrowserCookieAccessGate.resetForTesting()
BrowserCookieAccessGate.recordDenied(for: .arc, now: start)
let allowed = try await BrowserCookieAccessGate.withExplicitRetry {
try await ProviderInteractionContext.$current.withValue(.userInitiated) {
try await OpenAIDashboardBrowserCookieImporter.runBoundedCookieLoad(deadline: deadline) {
KeychainAccessGate.withTaskOverrideForTesting(false) {
ProviderInteractionContext.current == .userInitiated &&
BrowserCookieAccessGate.shouldAttempt(.arc, now: start.addingTimeInterval(1))
}
}
}
}
#expect(allowed)
}
}
@Test
func `timed out cookie cache work stays ordered before retry`() async throws {
let log = CookieOperationLog()
let firstOperationStarted = DispatchSemaphore(value: 0)
let allowFirstOperationToFinish = DispatchSemaphore(value: 0)
do {
_ = try await OpenAIDashboardBrowserCookieImporter.runBoundedCookieCacheOperation(
deadline: Date().addingTimeInterval(0.05))
{
log.append("first-start")
firstOperationStarted.signal()
_ = allowFirstOperationToFinish.wait(timeout: .now() + 5)
log.append("first-end")
return true
}
Issue.record("Expected first cache operation timeout")
} catch let error as URLError {
#expect(error.code == .timedOut)
} catch {
Issue.record("Unexpected error: \(error)")
}
let firstOperationStartResult = await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
continuation.resume(returning: firstOperationStarted.wait(timeout: .now() + 5))
}
}
#expect(firstOperationStartResult == .success)
do {
_ = try await OpenAIDashboardBrowserCookieImporter.runBoundedCookieCacheOperation(
deadline: Date().addingTimeInterval(0.05))
{
log.append("second")
return true
}
Issue.record("Expected retry to wait behind first cache operation")
} catch let error as URLError {
#expect(error.code == .timedOut)
} catch {
Issue.record("Unexpected error: \(error)")
}
allowFirstOperationToFinish.signal()
_ = try await OpenAIDashboardBrowserCookieImporter.runBoundedCookieCacheOperation(
deadline: Date().addingTimeInterval(1)) { true }
#expect(log.snapshot == ["first-start", "first-end", "second"])
}
@Test @MainActor
func `slow callback times out before completion`() async throws {
let start = Date()
let timeoutProbe = CookieTimeoutProbe()
do {
try await OpenAIDashboardBrowserCookieImporter.runBoundedCallback(
deadline: start.addingTimeInterval(0.05),
timeoutObserver: timeoutProbe.record)
{ completion in
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) {
completion()
}
}
Issue.record("Expected callback timeout")
} catch let error as URLError {
#expect(error.code == .timedOut)
} catch {
Issue.record("Unexpected error: \(error)")
}
let firedAt = try #require(timeoutProbe.firedAt)
#expect(firedAt.timeIntervalSince(start) < 0.3)
}
@Test @MainActor
func `slow value callback times out before completion`() async throws {
let start = Date()
let timeoutProbe = CookieTimeoutProbe()
do {
let _: [String] = try await OpenAIDashboardBrowserCookieImporter.runBoundedValueCallback(
deadline: start.addingTimeInterval(0.05),
timeoutObserver: timeoutProbe.record)
{ completion in
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) {
completion([])
}
}
Issue.record("Expected value callback timeout")
} catch let error as URLError {
#expect(error.code == .timedOut)
} catch {
Issue.record("Unexpected error: \(error)")
}
let firedAt = try #require(timeoutProbe.firedAt)
#expect(firedAt.timeIntervalSince(start) < 0.3)
}
@Test @MainActor
func `retry waits for timed out cookie store mutation`() async throws {
let keyOwner = NSObject()
let key = ObjectIdentifier(keyOwner)
let first = CookieCallbackHarness()
do {
try await OpenAIDashboardBrowserCookieImporter.runSerializedCallback(
key: key,
deadline: Date().addingTimeInterval(0.05),
start: first.capture)
Issue.record("Expected first mutation timeout")
} catch let error as URLError {
#expect(error.code == .timedOut)
}
let secondStarted = CookieCallbackFlag()
let second = Task { @MainActor in
try await OpenAIDashboardBrowserCookieImporter.runSerializedCallback(
key: key,
deadline: Date().addingTimeInterval(1))
{ completion in
secondStarted.set()
completion()
}
}
try await Task.sleep(for: .milliseconds(50))
#expect(!secondStarted.value)
first.finish()
try await second.value
#expect(secondStarted.value)
}
@Test
func `mismatch error mentions source label`() {
let err = OpenAIDashboardBrowserCookieImporter.ImportError.noMatchingAccount(
found: [
.init(sourceLabel: "Safari", email: "a@example.com"),
.init(sourceLabel: "Chrome", email: "b@example.com"),
])
let msg = err.localizedDescription
#expect(msg.contains("Safari=a@example.com"))
#expect(msg.contains("Chrome=b@example.com"))
}
@Test
func `timed out persistent validation keeps verified session`() {
let failure = OpenAIDashboardBrowserCookieImporter.persistentValidationFailure(URLError(.timedOut))
#expect(OpenAIDashboardBrowserCookieImporter.shouldTrustVerifiedSession(
afterPersistFailure: failure))
}
@Test
func `raw cookie mutation timeout is not trusted`() {
#expect(!OpenAIDashboardBrowserCookieImporter.shouldTrustVerifiedSession(
afterPersistFailure: URLError(.timedOut)))
}
@Test
func `non-timeout persistent validation failures are not trusted`() {
#expect(!OpenAIDashboardBrowserCookieImporter.shouldTrustVerifiedSession(
afterPersistFailure: OpenAIDashboardBrowserCookieImporter.ImportError.dashboardStillRequiresLogin))
}
}