import Foundation import ManaCore import Testing @testable import ManaAuthUI @Suite("TwoFactorEnrollmentViewModel") @MainActor struct TwoFactorEnrollmentViewModelTests { private func signedInAuth() async -> MockedAuth { let mocked = makeMockedAuth() let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig" mocked.setHandler { _ in (200, Data(#"{"accessToken":"\#(access)","refreshToken":"session-tok"}"#.utf8)) } await mocked.auth.signIn(email: "u@x.de", password: "pw") return mocked } @Test("enrollWithPassword erfolgreich → phase wechselt auf verify") func enrollSuccess() async { let mocked = await signedInAuth() let model = TwoFactorEnrollmentViewModel(auth: mocked.auth) model.password = "pw" mocked.setHandler { _ in (200, Data(#""" {"totpURI":"otpauth://totp/Mana:u@x.de?secret=ABC","backupCodes":["a","b","c"]} """#.utf8)) } await model.enrollWithPassword() if case let .verify(uri, codes) = model.phase { #expect(uri.hasPrefix("otpauth://totp/")) #expect(codes == ["a", "b", "c"]) } else { Issue.record("Expected .verify, got \(model.phase)") } #expect(model.password == "") // out of memory } @Test("enrollWithPassword falsches PW → .error, phase bleibt password") func enrollWrongPassword() async { let mocked = await signedInAuth() let model = TwoFactorEnrollmentViewModel(auth: mocked.auth) model.password = "wrong" mocked.setHandler { _ in (401, Data(#"{"error":"INVALID_CREDENTIALS","status":401}"#.utf8)) } await model.enrollWithPassword() if case .password = model.phase { #expect(Bool(true)) } else { Issue.record("Expected .password, got \(model.phase)") } if case let .error(message) = model.status { #expect(message == "Email oder Passwort falsch") } else { Issue.record("Expected .error, got \(model.status)") } } @Test("canSubmitVerify fordert 6 Ziffern") func canSubmitVerify() async { let mocked = await signedInAuth() let model = TwoFactorEnrollmentViewModel(auth: mocked.auth) model.password = "pw" mocked.setHandler { _ in (200, Data(#""" {"totpURI":"otpauth://totp/X","backupCodes":["a"]} """#.utf8)) } await model.enrollWithPassword() model.verifyCode = "" #expect(model.canSubmitVerify == false) model.verifyCode = "12345" #expect(model.canSubmitVerify == false) model.verifyCode = "123456" #expect(model.canSubmitVerify == true) model.verifyCode = "abcdef" #expect(model.canSubmitVerify == false) } @Test("confirmVerify wechselt von verify auf backupCodes") func confirmVerifySwitchesPhase() async { let mocked = await signedInAuth() let model = TwoFactorEnrollmentViewModel(auth: mocked.auth) model.password = "pw" mocked.setHandler { _ in (200, Data(#""" {"totpURI":"otpauth://totp/X","backupCodes":["a","b","c"]} """#.utf8)) } await model.enrollWithPassword() model.verifyCode = "123456" model.confirmVerify() if case let .backupCodes(codes) = model.phase { #expect(codes == ["a", "b", "c"]) } else { Issue.record("Expected .backupCodes, got \(model.phase)") } #expect(model.verifyCode == "") } @Test("backupCodes computed property returnt Codes aus verify- und backupCodes-Phase") func backupCodesAccessor() async { let mocked = await signedInAuth() let model = TwoFactorEnrollmentViewModel(auth: mocked.auth) // Phase .password → keine Codes #expect(model.backupCodes == []) model.password = "pw" mocked.setHandler { _ in (200, Data(#""" {"totpURI":"otpauth://totp/X","backupCodes":["c1","c2"]} """#.utf8)) } await model.enrollWithPassword() // Phase .verify → Codes verfügbar #expect(model.backupCodes == ["c1", "c2"]) model.verifyCode = "123456" model.confirmVerify() // Phase .backupCodes → Codes weiter verfügbar #expect(model.backupCodes == ["c1", "c2"]) } }