376 lines
13 KiB
Swift
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))
|
|
}
|
|
}
|