Files
2026-09-13 12:15:36 -07:00

117 lines
4.5 KiB
Swift

//
// 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) {}
}