152 lines
6.1 KiB
Swift
152 lines
6.1 KiB
Swift
// KeychainTests.swift
|
|
// Unit tests for the Keychain wrapper.
|
|
//
|
|
// IMPORTANT: Each test uses a unique key (never the production key
|
|
// `Keychain.credentialKey`) so real app data is never touched.
|
|
// All test keys are deleted in tearDown even if the test fails.
|
|
//
|
|
// Note: Keychain tests require the iOS simulator or a real device with
|
|
// a valid entitlement. They will fail in headless environments where
|
|
// the Security framework is unavailable.
|
|
|
|
import XCTest
|
|
@testable import piRemote
|
|
|
|
final class KeychainTests: XCTestCase {
|
|
|
|
// MARK: - Helpers
|
|
|
|
/// Unique per-run key prefix so parallel test runs don't collide.
|
|
private var testKey: String { "test.keychain.\(name)" }
|
|
|
|
override func tearDown() {
|
|
// Always clean up the test key to avoid polluting the Keychain.
|
|
Keychain.shared.delete(key: testKey)
|
|
super.tearDown()
|
|
}
|
|
|
|
/// A minimal `SidecarCredential` for testing.
|
|
private func makeCredential(name: String = "test-pi") -> SidecarCredential {
|
|
SidecarCredential(
|
|
sidecarId: "sid-\(UUID().uuidString)",
|
|
host: "192.168.1.100",
|
|
port: 7777,
|
|
bearerToken: "bearer-\(UUID().uuidString)",
|
|
tlsFingerprint: "deadbeef1234",
|
|
sidecarName: name,
|
|
pairedAt: Date(timeIntervalSince1970: 1_716_000_000)
|
|
)
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: 1. Save + load round-trip
|
|
// =========================================================================
|
|
|
|
func testSaveAndLoad_credential_roundTrips() throws {
|
|
let credential = makeCredential(name: "my-pi")
|
|
|
|
try Keychain.shared.save(credential, key: testKey)
|
|
let loaded: SidecarCredential = try Keychain.shared.load(key: testKey)
|
|
|
|
XCTAssertEqual(loaded.sidecarId, credential.sidecarId)
|
|
XCTAssertEqual(loaded.host, credential.host)
|
|
XCTAssertEqual(loaded.port, credential.port)
|
|
XCTAssertEqual(loaded.bearerToken, credential.bearerToken)
|
|
XCTAssertEqual(loaded.tlsFingerprint, credential.tlsFingerprint)
|
|
XCTAssertEqual(loaded.sidecarName, credential.sidecarName)
|
|
// Date comparison within 1 second tolerance (JSON Date encoding).
|
|
XCTAssertEqual(loaded.pairedAt.timeIntervalSince1970,
|
|
credential.pairedAt.timeIntervalSince1970,
|
|
accuracy: 1.0)
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: 2. Update (upsert)
|
|
// =========================================================================
|
|
|
|
func testSave_overwritesPrevious_onLoad() throws {
|
|
let first = makeCredential(name: "first-pi")
|
|
let second = makeCredential(name: "second-pi")
|
|
|
|
try Keychain.shared.save(first, key: testKey)
|
|
try Keychain.shared.save(second, key: testKey)
|
|
|
|
let loaded: SidecarCredential = try Keychain.shared.load(key: testKey)
|
|
XCTAssertEqual(loaded.sidecarName, "second-pi",
|
|
"Second save must overwrite the first")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: 3. Missing key → notFound
|
|
// =========================================================================
|
|
|
|
func testLoad_missingKey_throwsNotFound() {
|
|
let missingKey = "test.keychain.definitely-absent-\(UUID())"
|
|
defer { Keychain.shared.delete(key: missingKey) }
|
|
|
|
XCTAssertThrowsError(try Keychain.shared.load(key: missingKey) as SidecarCredential) { error in
|
|
guard let keychainError = error as? KeychainError,
|
|
case .notFound = keychainError else {
|
|
XCTFail("Expected KeychainError.notFound, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: 4. Delete clears entry
|
|
// =========================================================================
|
|
|
|
func testDelete_clearsEntry() throws {
|
|
try Keychain.shared.save(makeCredential(), key: testKey)
|
|
|
|
Keychain.shared.delete(key: testKey)
|
|
|
|
XCTAssertThrowsError(try Keychain.shared.load(key: testKey) as SidecarCredential,
|
|
"After delete, load must throw") { error in
|
|
guard let keychainError = error as? KeychainError,
|
|
case .notFound = keychainError else {
|
|
XCTFail("Expected KeychainError.notFound after delete, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func testDelete_missingKey_isNoOp() {
|
|
// Deleting a key that doesn't exist must not crash or throw.
|
|
Keychain.shared.delete(key: "test.keychain.never-saved-\(UUID())")
|
|
// No assertion needed — reaching this line means no crash.
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: 5. Production key is NOT used in tests
|
|
// =========================================================================
|
|
|
|
func testProductionKeyIsUntouched() {
|
|
// This test simply verifies that our test key is different from the
|
|
// production credential key, so tests never corrupt real data.
|
|
XCTAssertNotEqual(testKey, Keychain.credentialKey,
|
|
"Test key must differ from the production credential key")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: 6. Generic save/load with a simple Codable type
|
|
// =========================================================================
|
|
|
|
private struct TestPayload: Codable, Equatable {
|
|
let id: String
|
|
let value: Int
|
|
}
|
|
|
|
func testSaveAndLoad_simplePayload() throws {
|
|
let key = "test.keychain.payload.\(UUID())"
|
|
let payload = TestPayload(id: "x", value: 99)
|
|
defer { Keychain.shared.delete(key: key) }
|
|
|
|
try Keychain.shared.save(payload, key: key)
|
|
let loaded: TestPayload = try Keychain.shared.load(key: key)
|
|
|
|
XCTAssertEqual(loaded, payload)
|
|
}
|
|
}
|