3-Phasen-Wizard für 2FA-Enrollment + Single-Step-Sheet für Disable. Setzt mana-swift-core ≥ 1.4.0 voraus. ManaTwoFactorEnrollView: 1. Passwort-Re-Auth → server liefert otpauth-URI + Backup-Codes 2. QR-Code-Display (CoreImage.CIFilter.qrCodeGenerator) + 6-stellige Test-Code-Eingabe 3. Backup-Codes-Liste mit Copy-to-Clipboard ManaTwoFactorDisableView: - Re-Auth via Passwort, destructive-Button, .done-Konfirmation 5 neue Tests, 44/44 grün. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.4 KiB
Swift
130 lines
4.4 KiB
Swift
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"])
|
|
}
|
|
}
|