v0.4.0 — ManaTwoFactorEnrollView + ManaTwoFactorDisableView
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>
This commit is contained in:
parent
c1555565b6
commit
dc8e5a4e9b
4 changed files with 595 additions and 0 deletions
130
Tests/ManaAuthUITests/TwoFactorEnrollmentViewModelTests.swift
Normal file
130
Tests/ManaAuthUITests/TwoFactorEnrollmentViewModelTests.swift
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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"])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue