import Foundation import ManaCore import Testing @testable import ManaAuthUI @Suite("TwoFactorChallengeViewModel") @MainActor struct TwoFactorChallengeViewModelTests { /// Bringt den AuthClient in den `.twoFactorRequired`-Status. private func challengedAuth() async -> MockedAuth { let mocked = makeMockedAuth() mocked.setHandler { _ in (200, Data(#""" {"twoFactorRequired":true,"twoFactorMethods":["totp"],"twoFactorToken":"tf-x"} """#.utf8)) } await mocked.auth.signIn(email: "u@x.de", password: "pw") return mocked } @Test("canSubmit fordert 6-stellige Ziffern im TOTP-Modus") func canSubmitTotpDigits() { let model = TwoFactorChallengeViewModel(auth: makeMockedAuth().auth) model.code = "" #expect(model.canSubmit == false) model.code = "12345" #expect(model.canSubmit == false) // 5 Ziffern model.code = "123456" #expect(model.canSubmit == true) model.code = "123 456" // erlaubt Whitespace → 6 Ziffern #expect(model.canSubmit == true) model.code = "abcdef" #expect(model.canSubmit == false) } @Test("canSubmit im Backup-Modus akzeptiert nicht-leere Strings") func canSubmitBackupCode() { let model = TwoFactorChallengeViewModel(auth: makeMockedAuth().auth) model.toggleMode() #expect(model.mode == .backupCode) model.code = "" #expect(model.canSubmit == false) model.code = "abc-def-ghi" #expect(model.canSubmit == true) } @Test("toggleMode wechselt mode + cleared code") func toggleModeClearsCode() { let model = TwoFactorChallengeViewModel(auth: makeMockedAuth().auth) model.code = "123456" model.toggleMode() #expect(model.mode == .backupCode) #expect(model.code == "") } @Test("submit mit erfolgreichem TOTP setzt AuthClient auf signedIn") func submitTotpSuccess() async { let mocked = await challengedAuth() let model = TwoFactorChallengeViewModel(auth: mocked.auth) model.code = "123456" let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig" mocked.setHandler { request in #expect(request.url?.path == "/api/v1/auth/two-factor/verify-totp") return (200, Data(#""" {"success":true,"accessToken":"\#(access)","refreshToken":"r1"} """#.utf8)) } await model.submit() #expect(mocked.auth.status == .signedIn(email: "u@x.de")) #expect(model.code == "") #expect(model.status == .idle) } @Test("submit mit falschem TOTP → .error, AuthClient bleibt twoFactorRequired") func submitTotpWrongCode() async { let mocked = await challengedAuth() let model = TwoFactorChallengeViewModel(auth: mocked.auth) model.code = "000000" mocked.setHandler { _ in (401, Data(#"{"error":"TWO_FACTOR_FAILED","status":401}"#.utf8)) } await model.submit() if case let .error(message) = model.status { #expect(message == "Zwei-Faktor-Code falsch.") } else { Issue.record("Expected .error, got \(model.status)") } // AuthClient bleibt im challenge-Status, User kann retry if case .twoFactorRequired = mocked.auth.status { #expect(Bool(true)) } else { Issue.record("Expected .twoFactorRequired, got \(mocked.auth.status)") } } @Test("submit im Backup-Modus ruft verify-backup-code-Endpoint") func submitBackupCodeRoutesCorrectly() async { let mocked = await challengedAuth() let model = TwoFactorChallengeViewModel(auth: mocked.auth) model.toggleMode() model.code = "abc-def-ghi" let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig" mocked.setHandler { request in #expect(request.url?.path == "/api/v1/auth/two-factor/verify-backup-code") return (200, Data(#""" {"success":true,"accessToken":"\#(access)","refreshToken":"r1"} """#.utf8)) } await model.submit() #expect(mocked.auth.status == .signedIn(email: "u@x.de")) } }