197 lines
7.7 KiB
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")
|
|
}
|
|
}
|