// ── BLAuthClient SmartAuth v2 Tests ───────────────────────── // Tests for social login, MFA verify methods. // Uses URLProtocol mocking to intercept network requests. import XCTest @testable import ByteLystPlatformSDK // MARK: - Mock URL Protocol private class MockURLProtocol: URLProtocol { static var mockResponses: [String: (Data, Int)] = [:] override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { let path = request.url?.path ?? "" let method = request.httpMethod ?? "GET" let key = "\(method) \(path)" if let (data, statusCode) = MockURLProtocol.mockResponses[key] { let response = HTTPURLResponse( url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: ["Content-Type": "application/json"] )! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) } else { let response = HTTPURLResponse( url: request.url!, statusCode: 404, httpVersion: nil, headerFields: nil )! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: Data()) } client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} } // MARK: - Tests final class BLAuthClientSmartAuthTests: XCTestCase { private var config: BLPlatformConfig! private var client: BLPlatformClient! private var authClient: BLAuthClient! override func setUp() { super.setUp() config = BLPlatformConfig( productId: "testapp", baseURL: "http://localhost:4003", platform: "ios", channel: "test", bundleId: "com.test.smartauth" ) // Configure URLSession with mock protocol wired into BLPlatformClient let sessionConfig = URLSessionConfiguration.ephemeral sessionConfig.protocolClasses = [MockURLProtocol.self] client = BLPlatformClient(config: config, sessionConfiguration: sessionConfig) authClient = BLAuthClient(config: config, client: client) MockURLProtocol.mockResponses.removeAll() } override func tearDown() { MockURLProtocol.mockResponses.removeAll() // Clean up keychain BLKeychain.delete(service: "com.test.smartauth", key: "access_token") BLKeychain.delete(service: "com.test.smartauth", key: "refresh_token") super.tearDown() } // MARK: - Test 1: Swift Social Login (Google) func testLoginWithGoogleCallsCorrectEndpoint() async throws { // Mock a successful token response for Google OAuth let tokenJSON = """ {"accessToken":"at_123","refreshToken":"rt_456","user":{"id":"u1","email":"test@google.com","displayName":"Test User","plan":"free","role":"user"}} """ MockURLProtocol.mockResponses["POST /api/auth/oauth/google"] = (tokenJSON.data(using: .utf8)!, 200) let user = try await authClient.loginWithGoogle(idToken: "mock_google_id_token") XCTAssertEqual(user.id, "u1") XCTAssertEqual(user.email, "test@google.com") XCTAssertEqual(user.displayName, "Test User") } func testLoginWithGoogleDetectsMfaChallenge() async { // Mock an MFA challenge response for Google OAuth let mfaJSON = """ {"mfaRequired":true,"mfaChallenge":"ch_google_123","methods":["totp"]} """ MockURLProtocol.mockResponses["POST /api/auth/oauth/google"] = (mfaJSON.data(using: .utf8)!, 200) do { _ = try await authClient.loginWithGoogle(idToken: "mock_google_id_token") XCTFail("Should have thrown BLAuthError.mfaRequired") } catch let error as BLAuthError { if case .mfaRequired(let challenge) = error { XCTAssertEqual(challenge.mfaChallenge, "ch_google_123") XCTAssertEqual(challenge.methods, ["totp"]) } else { XCTFail("Expected mfaRequired error") } } } // MARK: - Test 2: Swift MFA Verify func testVerifyMfaCallsCorrectEndpoint() async throws { // Mock a successful MFA verification response let tokenJSON = """ {"accessToken":"at_mfa","refreshToken":"rt_mfa","user":{"id":"u2","email":"mfa@test.com","displayName":"MFA User"}} """ MockURLProtocol.mockResponses["POST /api/auth/mfa/verify"] = (tokenJSON.data(using: .utf8)!, 200) let user = try await authClient.verifyMfa( challengeToken: "mock_challenge_token", code: "123456", method: "totp" ) XCTAssertEqual(user.id, "u2") XCTAssertEqual(user.email, "mfa@test.com") } // MARK: - Type Verification Tests func testSmartAuthTypesAreDecodable() throws { // BLMfaChallenge let challengeJSON = """ {"mfaRequired":true,"mfaChallenge":"ch_abc123","methods":["totp"]} """ let challenge = try JSONDecoder().decode(BLMfaChallenge.self, from: challengeJSON.data(using: .utf8)!) XCTAssertTrue(challenge.mfaRequired) XCTAssertEqual(challenge.mfaChallenge, "ch_abc123") XCTAssertEqual(challenge.methods, ["totp"]) // BLMfaStatus let statusJSON = """ {"mfaEnabled":true,"methods":["totp"],"recoveryCodesRemaining":6} """ let status = try JSONDecoder().decode(BLMfaStatus.self, from: statusJSON.data(using: .utf8)!) XCTAssertTrue(status.mfaEnabled) XCTAssertEqual(status.recoveryCodesRemaining, 6) // BLAuthProvider let providerJSON = """ {"provider":"google","email":"test@test.com","linkedAt":"2026-01-01T00:00:00Z","lastUsedAt":null} """ let provider = try JSONDecoder().decode(BLAuthProvider.self, from: providerJSON.data(using: .utf8)!) XCTAssertEqual(provider.provider, "google") XCTAssertNil(provider.lastUsedAt) // BLDevice let deviceJSON = """ {"id":"dev_1","name":"iPhone 16","platform":"ios","trustLevel":"trusted","trustExpiresAt":"2026-06-01T00:00:00Z","lastLoginAt":"2026-03-01T00:00:00Z"} """ let device = try JSONDecoder().decode(BLDevice.self, from: deviceJSON.data(using: .utf8)!) XCTAssertEqual(device.trustLevel, "trusted") XCTAssertEqual(device.platform, "ios") // BLLoginEvent let eventJSON = """ {"id":"evt_1","eventType":"login_success","method":"google","ip":"1.2.3.4","geo":{"country":"US","city":"SF"},"riskScore":15,"createdAt":"2026-03-01T00:00:00Z"} """ let event = try JSONDecoder().decode(BLLoginEvent.self, from: eventJSON.data(using: .utf8)!) XCTAssertEqual(event.riskScore, 15) XCTAssertEqual(event.geo?.city, "SF") } func testAuthStateIncludesMfaRequired() { let challenge = BLMfaChallenge(mfaRequired: true, mfaChallenge: "ch_test", methods: ["totp"]) let state = BLAuthState.mfaRequired(challenge) if case .mfaRequired(let c) = state { XCTAssertEqual(c.mfaChallenge, "ch_test") } else { XCTFail("Expected mfaRequired state") } } func testBLAuthErrorMfaRequired() { let challenge = BLMfaChallenge(mfaRequired: true, mfaChallenge: "ch_test", methods: ["totp", "recovery"]) let error = BLAuthError.mfaRequired(challenge) XCTAssertEqual(error.localizedDescription, "Multi-factor authentication required") } }