learning_ai_common_plat/packages/swift-platform-sdk/Tests/BLAuthClientSmartAuthTests.swift

197 lines
7.7 KiB
Swift

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