// // ClientAndAuthTests.swift // PortalKitTests // // Verification of CredentialStorage abstractions, PortalAuth, and PortalClient models. // import XCTest @testable import PortalKit final class ClientAndAuthTests: XCTestCase { func testInMemoryCredentialStorage() { let storage = InMemoryCredentialStorage() XCTAssertNil(storage.getAuthToken()) XCTAssertNil(storage.getPinnedCertSha256()) storage.save(token: "test_token_123", certSha256: "AABBCCDDEEFF") XCTAssertEqual(storage.getAuthToken(), "test_token_123") XCTAssertEqual(storage.getPinnedCertSha256(), "aabbccddeeff") // lowercased storage.clear() XCTAssertNil(storage.getAuthToken()) XCTAssertNil(storage.getPinnedCertSha256()) } func testFileCredentialStorage() { let tempDir = FileManager.default.temporaryDirectory let tempFile = tempDir.appendingPathComponent("test_credentials_\(UUID().uuidString).json") defer { try? FileManager.default.removeItem(at: tempFile) } let storage = FileCredentialStorage(fileURL: tempFile) XCTAssertNil(storage.getAuthToken()) storage.save(token: "tok_abc", certSha256: "010203040506") XCTAssertEqual(storage.getAuthToken(), "tok_abc") XCTAssertEqual(storage.getPinnedCertSha256(), "010203040506") // Create fresh instance pointing to same file let reloadStorage = FileCredentialStorage(fileURL: tempFile) XCTAssertEqual(reloadStorage.getAuthToken(), "tok_abc") XCTAssertEqual(reloadStorage.getPinnedCertSha256(), "010203040506") reloadStorage.clear() XCTAssertNil(reloadStorage.getAuthToken()) } func testPortalAuthFacade() { let original = PortalAuth.defaultStorage defer { PortalAuth.defaultStorage = original } let mockStorage = InMemoryCredentialStorage() PortalAuth.defaultStorage = mockStorage PortalAuth.save(token: "facade_token", certSha256: "CAFEBABE") XCTAssertEqual(PortalAuth.token, "facade_token") XCTAssertEqual(PortalAuth.pinnedCertSha256, "cafebabe") PortalAuth.clear() XCTAssertNil(PortalAuth.token) XCTAssertNil(PortalAuth.pinnedCertSha256) } func testPortalClientHostNormalization() { let c1 = PortalClient(host: "10.0.0.10") XCTAssertEqual(c1.host, "10.0.0.10:\(PortalEndpoints.port)") let c2 = PortalClient(host: "https://10.0.0.10:9000/") XCTAssertEqual(c2.host, "10.0.0.10:9000") let c3 = PortalClient(host: "http://myportal.local:\(PortalEndpoints.port)") XCTAssertEqual(c3.host, "myportal.local:\(PortalEndpoints.port)") } func testPortalClientControlRequiresPairing() async { let storage = InMemoryCredentialStorage() let client = PortalClient(host: "127.0.0.1", credentialStorage: storage) do { _ = try await client.control(command: "mode Desk") XCTFail("Should throw notPaired error") } catch { guard case PortalClientError.notPaired = error else { XCTFail("Expected notPaired error, got \(error)") return } } } func testTlsPinningChallengeEvaluationNonServerTrust() { let space = URLProtectionSpace( host: "localhost", port: PortalEndpoints.port, protocol: "https", realm: nil, authenticationMethod: NSURLAuthenticationMethodHTTPBasic ) let challenge = URLAuthenticationChallenge(protectionSpace: space, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: DummyChallengeSender()) let exp = expectation(description: "Challenge evaluated") PortalTlsPinning.evaluate(challenge: challenge, pinnedFingerprint: "somehash") { disposition, credential in XCTAssertEqual(disposition, .cancelAuthenticationChallenge) XCTAssertNil(credential) exp.fulfill() } wait(for: [exp], timeout: 1) } } private final class DummyChallengeSender: NSObject, URLAuthenticationChallengeSender { func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} func cancel(_ challenge: URLAuthenticationChallenge) {} func performDefaultHandling(for challenge: URLAuthenticationChallenge) {} func rejectProtectionSpaceAndContinue(with challenge: URLAuthenticationChallenge) {} }