Section 11/161 menit

11. Testing Passkeys di Simulator dan Device

11. Testing Passkeys di Simulator dan Device

Testing di Simulator (Xcode 15+)

swift
// Simulator mendukung passkey sejak Xcode 15
// Aktifkan: Simulator → Features → Face ID (untuk simulasi biometrik)

// Dalam test, gunakan real passkey flow — simulator mendukung penuh
func testPasskeyRegistration() async throws {
    // Test di simulator: biometrik akan prompt Face ID enrollment dialog
    // Terima di Features → Face ID → Enrolled
    
    // Gunakan actual ASAuthorizationController — tidak perlu mock khusus
    let viewModel = RegistrationViewModel()
    await viewModel.registerPasskey(username: "test@example.com")
    
    XCTAssertNotNil(viewModel.registeredCredentialID)
}

Protocol-Based Testing Architecture

swift
// Protocol untuk dependency injection
protocol PasskeyManagerProtocol {
    func register(username: String, challenge: Data) async throws -> RegistrationResult
    func signIn(challenge: Data) async throws -> AssertionResult
}

// Implementasi nyata
class PasskeyManager: PasskeyManagerProtocol {
    func register(username: String, challenge: Data) async throws -> RegistrationResult {
        let coordinator = AuthenticationCoordinator()
        let credential = try await coordinator.registerPasskey(
            username: username,
            userID: Data(username.utf8),
            challenge: challenge,
            rpID: "example.com"
        )
        return RegistrationResult(from: credential)
    }
    
    func signIn(challenge: Data) async throws -> AssertionResult {
        let coordinator = AuthenticationCoordinator()
        let assertion = try await coordinator.signInWithPasskey(
            challenge: challenge,
            rpID: "example.com"
        )
        return AssertionResult(from: assertion)
    }
}

// Mock untuk unit test
class MockPasskeyManager: PasskeyManagerProtocol {
    var shouldSucceed = true
    var registrationResult: RegistrationResult?
    var assertionResult: AssertionResult?

    func register(username: String, challenge: Data) async throws -> RegistrationResult {
        if !shouldSucceed { throw ASAuthorizationError(.failed) }
        return registrationResult ?? .mockSuccess
    }
    
    func signIn(challenge: Data) async throws -> AssertionResult {
        if !shouldSucceed { throw ASAuthorizationError(.canceled) }
        return assertionResult ?? .mockSuccess
    }
}

// ViewModel yang testable
class LoginViewModel {
    private let passkeyManager: PasskeyManagerProtocol

    init(passkeyManager: PasskeyManagerProtocol = PasskeyManager()) {
        self.passkeyManager = passkeyManager
    }

    var isSignedIn = false
    var errorMessage: String?

    func signIn() async {
        do {
            let challenge = try await server.fetchChallenge()
            let result = try await passkeyManager.signIn(challenge: challenge)
            try await server.verifyAssertion(result)
            isSignedIn = true
        } catch let error as ASAuthorizationError where error.code == .canceled {
            // User cancel — tidak tampilkan error
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

// Unit test tanpa simulator atau device
func testSignInSuccess() async {
    let mock = MockPasskeyManager()
    mock.shouldSucceed = true
    let viewModel = LoginViewModel(passkeyManager: mock)
    
    await viewModel.signIn()
    
    XCTAssertTrue(viewModel.isSignedIn)
    XCTAssertNil(viewModel.errorMessage)
}

func testSignInCanceled() async {
    let mock = MockPasskeyManager()
    mock.shouldSucceed = false
    let viewModel = LoginViewModel(passkeyManager: mock)
    
    await viewModel.signIn()
    
    XCTAssertFalse(viewModel.isSignedIn)
    XCTAssertNil(viewModel.errorMessage)  // Cancel tidak tampilkan error
}