import XCTest @testable import ReynaCLIHostCore // Tests for calendar.request_full_access – TDD, fake providers only final class CalendarAuthorizationTests: XCTestCase { // Fake auth providers struct AlreadyAuthorizedProvider: CalendarAuthorizationProviding { var requested = false func authorizationStatus() -> CalendarAuthorizationStatus { .authorized } func requestFullAccess() throws -> Bool { XCTFail("requestFullAccess must not be called when already authorized") return false } } struct NotDeterminedGrantedProvider: CalendarAuthorizationProviding { var statusCall = 0 func authorizationStatus() -> CalendarAuthorizationStatus { .notDetermined } func requestFullAccess() throws -> Bool { true } } struct NotDeterminedDeniedProvider: CalendarAuthorizationProviding { func authorizationStatus() -> CalendarAuthorizationStatus { .notDetermined } func requestFullAccess() throws -> Bool { false } } struct DeniedProvider: CalendarAuthorizationProviding { func authorizationStatus() -> CalendarAuthorizationStatus { .denied } func requestFullAccess() throws -> Bool { false } } struct TimeoutProvider: CalendarAuthorizationProviding { func authorizationStatus() -> CalendarAuthorizationStatus { .notDetermined } func requestFullAccess() throws -> Bool { throw CalendarProviderError.unavailable("calendar authorization timed out") } } struct ErrorProvider: CalendarAuthorizationProviding { func authorizationStatus() -> CalendarAuthorizationStatus { .notDetermined } func requestFullAccess() throws -> Bool { throw CalendarProviderError.unavailable("disk error") } } struct EmptyListProvider: CalendarListProviding { func listCalendars() throws -> [CalendarListItem] { [] } } // already-full permission returns state authorized without asking func testAlreadyAuthorizedReturnsAuthorizedWithoutRequesting() { let auth = AlreadyAuthorizedProvider() let req = Request(id: "a1", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: auth) XCTAssertTrue(resp.ok) XCTAssertEqual(resp.result?.status, "authorized") XCTAssertEqual(resp.result?.operation, "calendar.request_full_access") XCTAssertEqual(resp.id, "a1") } // notDetermined reaches request path func testNotDeterminedReachesRequestPath() { final class TrackingProvider: CalendarAuthorizationProviding, @unchecked Sendable { var didRequest = false var status: CalendarAuthorizationStatus = .notDetermined func authorizationStatus() -> CalendarAuthorizationStatus { status } func requestFullAccess() throws -> Bool { didRequest = true return true } } let tracking = TrackingProvider() let req = Request(id: "a2", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: tracking) XCTAssertTrue(tracking.didRequest, "requestFullAccess must be called when notDetermined") XCTAssertTrue(resp.ok) } // granted response returns {status:"authorized"} func testGrantedReturnsAuthorizedResult() { let auth = NotDeterminedGrantedProvider() let req = Request(id: "a3", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: auth) XCTAssertTrue(resp.ok) XCTAssertEqual(resp.result?.status, "authorized") XCTAssertEqual(resp.result?.protocol_version, PROTOCOL_VERSION) XCTAssertNil(resp.error) XCTAssertNil(resp.result?.calendars, "must not output calendar content") } // denied returns structured permission_denied func testDeniedReturnsPermissionDenied() { let auth = NotDeterminedDeniedProvider() let req = Request(id: "a4", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: auth) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "permission_denied") XCTAssertNotNil(resp.error?.message) XCTAssertNil(resp.result) } // denied when already denied also permission_denied func testAlreadyDeniedPathAlsoDenies() { let auth = DeniedProvider() let req = Request(id: "a5", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: auth) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "permission_denied") } // async timeout/error returns calendar_unavailable func testTimeoutReturnsCalendarUnavailable() { let auth = TimeoutProvider() let req = Request(id: "a6", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: auth) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "calendar_unavailable") XCTAssertTrue(resp.error?.message.lowercased().contains("timed out") ?? false) } func testErrorReturnsCalendarUnavailable() { let auth = ErrorProvider() let req = Request(id: "a7", operation: "calendar.request_full_access", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: EmptyListProvider(), authProvider: auth) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "calendar_unavailable") } // event/calendar list cannot call request method (verify list path never requests) func testCalendarListDoesNotCallAuthRequest() { final class SpyListProvider: CalendarListProviding, @unchecked Sendable { var called = false func listCalendars() throws -> [CalendarListItem] { called = true return [] } } final class SpyAuthProvider: CalendarAuthorizationProviding, @unchecked Sendable { var didCallStatus = false var didCallRequest = false func authorizationStatus() -> CalendarAuthorizationStatus { didCallStatus = true return .authorized } func requestFullAccess() throws -> Bool { didCallRequest = true return false } } let list = SpyListProvider() let auth = SpyAuthProvider() let req = Request(id: "list-1", operation: "calendar.list", arguments: .object([:])) let resp = dispatch(request: req, calendarProvider: list, authProvider: auth) XCTAssertTrue(resp.ok) XCTAssertFalse(auth.didCallRequest, "calendar.list must never call requestFullAccess") XCTAssertFalse(auth.didCallStatus, "calendar.list must not touch auth provider") XCTAssertTrue(list.called) } // MARK: - Shared holder to satisfy Swift 6 Sendable checks final class TestBox: @unchecked Sendable { var value: T init(_ v: T) { value = v } } // MARK: - EventKitMainRunLoopBridge – deterministic pump tests (no real EventKit/TCC) func testBridgePumpsMainRunLoopAndDeliversMainQueueCallback() throws { let exp = expectation(description: "bridge completes") let grantedBox = TestBox(false) let errorBox = TestBox(nil) DispatchQueue.main.async { let bridge = EventKitMainRunLoopBridge() do { // Deterministic seam: schedule callback onto next main run loop turn via Timer, // simulating EventKit delivering completion on main run loop. grantedBox.value = try bridge.requestAccess(timeout: 2) { completion in // Timer on main run loop – only fires when run loop is pumped Timer.scheduledTimer(withTimeInterval: 0.02, repeats: false) { _ in completion(true, nil) } } } catch { errorBox.value = error } exp.fulfill() } wait(for: [exp], timeout: 5) XCTAssertNil(errorBox.value, "bridge must not timeout when it pumps main run loop; got \(String(describing: errorBox.value))") XCTAssertTrue(grantedBox.value, "granted should be true after main-queue callback is pumped") } func testSemaphoreDeadlocksButBridgeDoesNot() throws { let semExp = expectation(description: "old impl would timeout") let completedBox = TestBox(false) DispatchQueue.main.async { let sem = DispatchSemaphore(value: 0) Timer.scheduledTimer(withTimeInterval: 0.02, repeats: false) { _ in completedBox.value = true sem.signal() } let res = sem.wait(timeout: .now() + 0.2) XCTAssertEqual(res, .timedOut, "Blocking semaphore on main thread must deadlock main-run-loop callback – proving old bug") XCTAssertFalse(completedBox.value, "Callback must not have run while semaphore blocked main loop") semExp.fulfill() } wait(for: [semExp], timeout: 2) } func testBridgeHandlesCompletionExactlyOnce() throws { let exp = expectation(description: "exactly once") let resultBox = TestBox(false) DispatchQueue.main.async { let bridge = EventKitMainRunLoopBridge() do { resultBox.value = try bridge.requestAccess(timeout: 1) { completion in completion(true, nil) completion(false, NSError(domain: "should-be-ignored", code: 1)) } } catch {} exp.fulfill() } wait(for: [exp], timeout: 2) XCTAssertTrue(resultBox.value, "First completion should win") } func testBridgeThreadSafetyForConcurrentCompletion() throws { let exp = expectation(description: "thread-safe") let grantedBox = TestBox(false) let doneBox = TestBox(false) DispatchQueue.main.async { let bridge = EventKitMainRunLoopBridge() do { grantedBox.value = try bridge.requestAccess(timeout: 1) { completion in DispatchQueue.global().async { completion(true, nil) } DispatchQueue.global().async { completion(false, nil) } } doneBox.value = true } catch {} exp.fulfill() } wait(for: [exp], timeout: 2) XCTAssertTrue(doneBox.value, "bridge must complete even with concurrent completions") } func testBridgePropagatesError() throws { let exp = expectation(description: "error propagation") let caughtBox = TestBox(false) DispatchQueue.main.async { let bridge = EventKitMainRunLoopBridge() do { _ = try bridge.requestAccess(timeout: 1) { completion in completion(false, NSError(domain: "test", code: 2, userInfo: [NSLocalizedDescriptionKey: "fake EK error"])) } } catch let err as CalendarProviderError { if case .unavailable(let msg) = err { caughtBox.value = msg.contains("fake EK error") } } catch {} exp.fulfill() } wait(for: [exp], timeout: 2) XCTAssertTrue(caughtBox.value, "Error from EK completion must be wrapped as calendar_unavailable") } func testBridgeTimeoutReturnsCorrectError() throws { let exp = expectation(description: "timeout") let codeBox = TestBox("") DispatchQueue.main.async { let bridge = EventKitMainRunLoopBridge() do { _ = try bridge.requestAccess(timeout: 0.15) { _ in } XCTFail("Should have thrown") } catch let err as CalendarProviderError { if case .unavailable(let msg) = err { codeBox.value = msg } } catch {} exp.fulfill() } wait(for: [exp], timeout: 2) XCTAssertTrue(codeBox.value.lowercased().contains("timed out"), "Timeout must produce 'calendar authorization timed out' message, got \(codeBox.value)") } // Production code location check: only CalendarAuthorizationProvider.swift calls requestFullAccessToEvents func testOnlyOneFileCallsRequestFullAccessToEvents() throws { let fm = FileManager.default // Walk up to find repo root containing native/ReynaCLIHost/Sources var cur = URL(fileURLWithPath: fm.currentDirectoryPath) var dirs: [URL] = [] for _ in 0..<10 { let cand = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHost") if fm.fileExists(atPath: cand.path) { dirs.append(cand) } let candCore = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHostCore") if fm.fileExists(atPath: candCore.path) { dirs.append(candCore) } let cand2 = cur.appendingPathComponent("Sources/ReynaCLIHost") if fm.fileExists(atPath: cand2.path) { dirs.append(cand2) } let candCore2 = cur.appendingPathComponent("Sources/ReynaCLIHostCore") if fm.fileExists(atPath: candCore2.path) { dirs.append(candCore2) } if !dirs.isEmpty { break } cur = cur.deletingLastPathComponent() } guard !dirs.isEmpty else { XCTFail("Could not locate Sources/ReynaCLIHost dir") return } var hits: [String] = [] for srcDir in dirs { let files = (try? fm.contentsOfDirectory(at: srcDir, includingPropertiesForKeys: nil)) ?? [] for file in files where file.pathExtension == "swift" { guard let content = try? String(contentsOf: file) else { continue } if content.contains("requestFullAccessToEvents") { hits.append(file.lastPathComponent) } } } // Dedupe + sort for stable assertion let uniqueSorted = Array(Set(hits)).sorted() XCTAssertEqual(uniqueSorted, ["CalendarAuthorizationProvider.swift"], "requestFullAccessToEvents must only appear in CalendarAuthorizationProvider.swift, found in \(uniqueSorted)") } func testNoOutputCalendarContentOnAuthOperations() { // Both authorized and denied paths must not include calendars let authOk = NotDeterminedGrantedProvider() let reqOk = Request(id: "ok", operation: "calendar.request_full_access", arguments: .object([:])) let respOk = dispatch(request: reqOk, calendarProvider: EmptyListProvider(), authProvider: authOk) XCTAssertNil(respOk.result?.calendars) let authDen = NotDeterminedDeniedProvider() let reqDen = Request(id: "den", operation: "calendar.request_full_access", arguments: .object([:])) let respDen = dispatch(request: reqDen, calendarProvider: EmptyListProvider(), authProvider: authDen) // denied has nil result, so no calendars by construction XCTAssertNil(respDen.result) } }