import Foundation import Testing @testable import Voice private final class AuthURLProtocol: URLProtocol { static var handler: ((URLRequest) throws -> (Int, Data))? override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { do { let (status, data) = try Self.handler!(request) client?.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil)!, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} } @Suite(.serialized) @MainActor struct AuthTests { private func config(_ value: String = "https://example.com") throws -> ServerConfig { try ServerConfig.parse(value).get() } private func session() -> URLSession { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [AuthURLProtocol.self] return URLSession(configuration: config) } @Test func certificateFailureHasSafeRecovery() throws { let url = URL(string: "https://192.168.86.2:9178")! let message = ConnectionFailure.message(URLError(.serverCertificateUntrusted), server: url) #expect(message.contains("192.168.86.2")) #expect(message.contains("Certificate Trust Settings")) #expect(!message.contains("disable")) #expect(ConnectionFailure.message(AuthError.status(401), server: url).contains("username and password")) } @Test func canonicalOriginsAndSecureIPv6() throws { let parsed = try config(" HTTPS://EXAMPLE.COM:443/path ") #expect(parsed.baseURL.absoluteString == "https://example.com") #expect(parsed.livekitURL.absoluteString == "wss://example.com/livekit") #expect(try config("https://[fd00::1]:9178").livekitURL.absoluteString == "wss://[fd00::1]:9178/livekit") for invalid in ["https://u:p@example.com", "https://example.com?token=x", "https://example.com#fragment", "http://192.168.1.1"] { if case .success = ServerConfig.parse(invalid) { Issue.record("Accepted unsafe origin") } } } @Test func bearerBootstrapAndMissingAuth() async throws { let c = try config() AuthURLProtocol.handler = { request in #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-session") #expect(request.url?.path == "/token") return (200, Data(#"{"token":"room-jwt","room":"room","user":{"id":"one","display_name":"One"}}"#.utf8)) } let response = try await TokenClient(config: c, bearerToken: "test-session", session: session()).fetchToken() #expect(response.user.id == "one") do { _ = try await TokenClient(config: c, session: session()).fetchToken() Issue.record("Unauthenticated bootstrap accepted") } catch { #expect(error is AuthError) } } @Test func loginMeLogoutWireContract() async throws { let c = try config() let user = #"{"id":"one","username":"alice","display_name":"Alice","role":"user","status":"ready"}"# AuthURLProtocol.handler = { request in switch request.url!.path { case "/api/auth/login": #expect(request.httpMethod == "POST") #expect(request.value(forHTTPHeaderField: "Authorization") == nil) return (200, Data("{\"token\":\"test-session\",\"user\":\(user),\"expires_at\":4102444800}".utf8)) case "/api/auth/me": #expect(request.httpMethod == "GET") #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-session") return (200, Data("{\"user\":\(user)}".utf8)) default: #expect(request.url!.path == "/api/auth/logout") #expect(request.httpMethod == "POST") #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-session") return (204, Data()) } } let memory = MemoryTokenStore() let client = AuthClient(config: c, session: session()) let store = AuthStore(config: c, client: client, tokens: memory) await store.login(username: "alice", password: "never-persist-this") #expect(store.user?.id == "one") #expect(try memory.token(origin: c.baseURL.absoluteString) == "test-session") #expect(try memory.token(origin: "https://another.example") == nil) let restored = AuthStore(config: c, client: client, tokens: memory) await restored.restore() #expect(restored.user?.username == "alice") await store.logout { #expect(store.token == nil) #expect(store.user?.id == "one") // old UI survives only until transport disconnect } #expect(store.user == nil) #expect(try memory.token(origin: c.baseURL.absoluteString) == nil) } @Test func transientRestoreFailureKeepsTokenButUnauthorizedDeletesIt() async throws { let c = try config() let memory = MemoryTokenStore() try memory.save("test-session", origin: c.baseURL.absoluteString) AuthURLProtocol.handler = { _ in throw URLError(.notConnectedToInternet) } let offline = AuthStore(config: c, client: AuthClient(config: c, session: session()), tokens: memory) await offline.restore() #expect(offline.user == nil) #expect(try memory.token(origin: c.baseURL.absoluteString) == "test-session") AuthURLProtocol.handler = { _ in (401, Data()) } let revoked = AuthStore(config: c, client: AuthClient(config: c, session: session()), tokens: memory) await revoked.restore() #expect(revoked.user == nil) #expect(try memory.token(origin: c.baseURL.absoluteString) == nil) } @Test func logoutInvalidatesSuspendedRestore() async throws { let c = try config() let memory = MemoryTokenStore() try memory.save("test-session", origin: c.baseURL.absoluteString) AuthURLProtocol.handler = { _ in return (200, Data(#"{"user":{"id":"one","username":"alice","role":"user","status":"ready"}}"#.utf8)) } let store = AuthStore(config: c, client: AuthClient(config: c, session: session()), tokens: memory) let restore = Task { await store.restore() } await Task.yield() await store.logout {} await restore.value #expect(store.user == nil) #expect(store.token == nil) } @Test func historyNeverImportsGlobalLegacyAndSeparatesAccounts() throws { let origin = "https://history-test.example" let legacy = AppPreferences.store.object(forKey: "voice_history") defer { if let legacy { AppPreferences.store.set(legacy, forKey: "voice_history") } else { AppPreferences.store.removeObject(forKey: "voice_history") } } AppPreferences.store.set("[{\"role\":\"user\",\"text\":\"legacy secret\",\"timestamp\":0}]", forKey: "voice_history") let first = TranscriptHistory(origin: origin, userID: "one") #expect(!first.entries.contains { $0.text == "legacy secret" }) first.clear() first.sync(finals: [("user", "private", Date())]) #expect(TranscriptHistory(origin: origin, userID: "one").entries.last?.text == "private") let other = TranscriptHistory(origin: origin, userID: "two") #expect(other.entries.isEmpty) #expect(TranscriptHistory(origin: "https://other.example", userID: "one").entries.isEmpty) first.clear() } @Test func progressCompletesWithoutTranscriptPollution() throws { let settings = SettingsStore() let c = try config() let voice = VoiceSession(config: c, tokenClient: TokenClient(config: c), settings: settings) let before = voice.transcriptHandler.events voice.receiveAssistantProgress(Data(#"{"type":"assistant_progress","request_id":"a","status":"working","message":"Checking your calendar…"}"#.utf8)) #expect(voice.assistantActivity == "Checking your calendar…") voice.receiveAssistantProgress(Data(#"{"type":"assistant_progress","request_id":"a","status":"completed","message":"Done"}"#.utf8)) #expect(voice.assistantActivity == nil) #expect(voice.transcriptHandler.events == before) } }