import Foundation import Testing @testable import ManaCore @Suite("AuthClient 2FA-Login-Challenge") @MainActor struct AuthClientTwoFactorTests { @Test("signIn mit 2FA-Account → .twoFactorRequired statt .signedIn") func signInRedirectsToTwoFactor() async { let mocked = makeMockedAuth() mocked.setHandler { _ in (200, Data(#""" {"twoFactorRequired":true,"twoFactorMethods":["totp"],"twoFactorToken":"tf-abc123"} """#.utf8)) } await mocked.auth.signIn(email: "u@x.de", password: "pw") if case let .twoFactorRequired(token, methods, email) = mocked.auth.status { #expect(token == "tf-abc123") #expect(methods == ["totp"]) #expect(email == "u@x.de") } else { Issue.record("Expected .twoFactorRequired, got \(mocked.auth.status)") } } @Test("verifyTotp erfolgreich → .signedIn") func verifyTotpSuccess() async throws { let mocked = makeMockedAuth() // Schritt 1: signIn liefert 2FA-Challenge. mocked.setHandler { _ in (200, Data(#""" {"twoFactorRequired":true,"twoFactorMethods":["totp"],"twoFactorToken":"tf-xyz"} """#.utf8)) } await mocked.auth.signIn(email: "u@x.de", password: "pw") // Schritt 2: verifyTotp liefert Tokens. let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig" let captured = MockURLProtocol.Capture() mocked.setHandler { request in captured.store(request) return (200, Data(#""" {"success":true,"accessToken":"\#(access)","refreshToken":"r1"} """#.utf8)) } try await mocked.auth.verifyTotp(code: "123456") #expect(mocked.auth.status == .signedIn(email: "u@x.de")) let request = try #require(captured.request) #expect(request.url?.path == "/api/v1/auth/two-factor/verify-totp") // Body trägt code + twoFactorToken aus dem Challenge-Status let body = request.httpBody ?? request.bodyStreamData() ?? Data() let json = try JSONSerialization.jsonObject(with: body) as? [String: Any] ?? [:] #expect(json["code"] as? String == "123456") #expect(json["twoFactorToken"] as? String == "tf-xyz") } @Test("verifyTotp ohne aktiven 2FA-Challenge wirft validation") func verifyTotpRequiresChallenge() async { let mocked = makeMockedAuth() // Status ist initial .unknown — kein 2FA-Challenge. do { try await mocked.auth.verifyTotp(code: "123456") Issue.record("Expected throw") } catch let AuthError.validation(message) { #expect(message?.contains("2FA-Challenge") == true) } catch { Issue.record("Unexpected error: \(error)") } } @Test("verifyTotp mit falschem Code → twoFactorFailed, bleibt im Challenge") func verifyTotpWrongCode() async { let mocked = makeMockedAuth() mocked.setHandler { _ in (200, Data(#""" {"twoFactorRequired":true,"twoFactorMethods":["totp"],"twoFactorToken":"tf-xyz"} """#.utf8)) } await mocked.auth.signIn(email: "u@x.de", password: "pw") mocked.setHandler { _ in (401, Data(#"{"error":"TWO_FACTOR_FAILED","status":401}"#.utf8)) } do { try await mocked.auth.verifyTotp(code: "000000") Issue.record("Expected throw") } catch AuthError.twoFactorFailed { // Status bleibt im Challenge — User kann erneut versuchen if case .twoFactorRequired = mocked.auth.status { #expect(Bool(true)) } else { Issue.record("Status should remain .twoFactorRequired, got \(mocked.auth.status)") } } catch { Issue.record("Unexpected error: \(error)") } } @Test("verifyBackupCode erfolgreich → .signedIn") func verifyBackupCodeSuccess() async throws { let mocked = makeMockedAuth() mocked.setHandler { _ in (200, Data(#""" {"twoFactorRequired":true,"twoFactorMethods":["totp"],"twoFactorToken":"tf-xyz"} """#.utf8)) } await mocked.auth.signIn(email: "u@x.de", password: "pw") let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig" let captured = MockURLProtocol.Capture() mocked.setHandler { request in captured.store(request) return (200, Data(#""" {"success":true,"accessToken":"\#(access)","refreshToken":"r1"} """#.utf8)) } try await mocked.auth.verifyBackupCode(code: "abc-def-ghi") #expect(mocked.auth.status == .signedIn(email: "u@x.de")) let request = try #require(captured.request) #expect(request.url?.path == "/api/v1/auth/two-factor/verify-backup-code") } }