v0.1.0 — initialer Sprint, vollständige Auth-Reise als SwiftUI
Phase 2 aus dem Native-Auth-Vollausbau-Plan (Option A, siehe ../mana/docs/MANA_SWIFT.md). Entstanden weil drei Apps fast- byte-identische LoginView.swift hatten und Sign-Up/Forgot-PW komplett fehlten. ManaAuthUI-Library mit: - ManaBrandConfig — App-injizierte Theme-Werte (forest für Cards/ Manaspur, mana-default für Memoro), Environment-Key, View-Modifier - Base-Components: ManaAuthScaffold, ManaPrimaryButton, ManaTextField, ManaSecureField + .manaEmailField()-Modifier - ManaLoginView + LoginViewModel — Email/PW-Login, schaltet bei AuthError.emailNotVerified automatisch auf ManaEmailVerifyGateView - ManaSignUpView + SignUpViewModel — Email/Name/PW + awaiting- Verification-Hinweis-Screen - ManaEmailVerifyGateView + ViewModel — Resend-Verification - ManaForgotPasswordView + ViewModel — Reset-Mail anfordern (immer generischer Hinweis, User-Enumeration-Schutz) - ManaResetPasswordView + ViewModel — neues PW mit Token aus Universal-Link - ManaChangeEmailView, ManaChangePasswordView, ManaDeleteAccountView + internal ViewModels — Account-Bausteine - ManaDeleteAccountView ist zweistufig (Bestätigungs-Wort tippen + Passwort) → App-Store-Guideline 5.1.1(v) Pflicht-Surface 26/26 ViewModel-Tests grün via per-test-ID URLProtocol-Routing (löst Parallel-Pollution zwischen .serialized Suites). 🤖 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:
commit
0a2cb349b4
29 changed files with 2614 additions and 0 deletions
104
Tests/ManaAuthUITests/AccountViewModelTests.swift
Normal file
104
Tests/ManaAuthUITests/AccountViewModelTests.swift
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import Testing
|
||||
@testable import ManaAuthUI
|
||||
|
||||
@Suite("Account-ViewModels (ChangeEmail/ChangePW/Delete)")
|
||||
@MainActor
|
||||
struct AccountViewModelTests {
|
||||
/// Simuliert eingeloggten User über den echten `signIn`-Pfad
|
||||
/// mit Mock-URL, weil `persistSession` internal in ManaCore ist.
|
||||
private func signedInAuth() async -> MockedAuth {
|
||||
let mocked = makeMockedAuth()
|
||||
let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig"
|
||||
mocked.setHandler { _ in
|
||||
(200, Data(#"{"accessToken":"\#(access)","refreshToken":"r"}"#.utf8))
|
||||
}
|
||||
await mocked.auth.signIn(email: "u@x.de", password: "pw")
|
||||
return mocked
|
||||
}
|
||||
|
||||
// MARK: - ChangeEmail
|
||||
|
||||
@Test("changeEmail erfolgreich → .done")
|
||||
func changeEmailSuccess() async {
|
||||
let mocked = await signedInAuth()
|
||||
let model = ChangeEmailViewModel(auth: mocked.auth, callbackUniversalLink: nil)
|
||||
model.newEmail = "neu@x.de"
|
||||
|
||||
mocked.setHandler { _ in (200, Data(#"{"success":true}"#.utf8)) }
|
||||
await model.submit()
|
||||
#expect(model.status == .done)
|
||||
}
|
||||
|
||||
@Test("changeEmail ohne Session → .error mit notSignedIn")
|
||||
func changeEmailNoSession() async {
|
||||
let model = ChangeEmailViewModel(auth: makeMockedAuth().auth, callbackUniversalLink: nil)
|
||||
model.newEmail = "neu@x.de"
|
||||
await model.submit()
|
||||
if case let .error(message) = model.status {
|
||||
#expect(message == "Nicht angemeldet")
|
||||
} else {
|
||||
Issue.record("Expected .error, got \(model.status)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ChangePassword
|
||||
|
||||
@Test("changePassword erfolgreich → .done und cleared Felder")
|
||||
func changePasswordSuccess() async {
|
||||
let mocked = await signedInAuth()
|
||||
let model = ChangePasswordViewModel(auth: mocked.auth)
|
||||
model.currentPassword = "alt-lang-genug"
|
||||
model.newPassword = "neu-lang-genug"
|
||||
model.confirmPassword = "neu-lang-genug"
|
||||
|
||||
mocked.setHandler { _ in (200, Data(#"{"success":true}"#.utf8)) }
|
||||
await model.submit()
|
||||
#expect(model.status == .done)
|
||||
#expect(model.currentPassword == "")
|
||||
#expect(model.newPassword == "")
|
||||
#expect(model.confirmPassword == "")
|
||||
}
|
||||
|
||||
@Test("changePassword Mismatch → canSubmit false")
|
||||
func changePasswordMismatch() {
|
||||
let model = ChangePasswordViewModel(auth: makeMockedAuth().auth)
|
||||
model.currentPassword = "alt"
|
||||
model.newPassword = "neu-lang-genug"
|
||||
model.confirmPassword = "anders-lang-genug"
|
||||
#expect(model.canSubmit == false)
|
||||
}
|
||||
|
||||
// MARK: - DeleteAccount
|
||||
|
||||
@Test("deleteAccount mit korrektem Bestätigungswort → .done")
|
||||
func deleteAccountSuccess() async {
|
||||
let mocked = await signedInAuth()
|
||||
let model = DeleteAccountViewModel(auth: mocked.auth)
|
||||
model.confirmationText = "LÖSCHEN"
|
||||
model.password = "pw"
|
||||
|
||||
mocked.setHandler { request in
|
||||
#expect(request.httpMethod == "DELETE")
|
||||
return (200, Data(#"{"success":true}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
#expect(model.status == .done)
|
||||
#expect(mocked.auth.status == .signedOut)
|
||||
}
|
||||
|
||||
@Test("deleteAccount mit falschem Bestätigungswort → canSubmit false")
|
||||
func deleteAccountWrongConfirmation() {
|
||||
let model = DeleteAccountViewModel(auth: makeMockedAuth().auth)
|
||||
model.confirmationText = "delete"
|
||||
model.password = "pw"
|
||||
#expect(model.canSubmit == false)
|
||||
model.confirmationText = "LÖSCHEN"
|
||||
#expect(model.canSubmit == true)
|
||||
// Case-insensitive
|
||||
model.confirmationText = "löschen"
|
||||
#expect(model.canSubmit == true)
|
||||
}
|
||||
}
|
||||
44
Tests/ManaAuthUITests/EmailVerifyGateViewModelTests.swift
Normal file
44
Tests/ManaAuthUITests/EmailVerifyGateViewModelTests.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import Testing
|
||||
@testable import ManaAuthUI
|
||||
|
||||
@Suite("EmailVerifyGateViewModel")
|
||||
@MainActor
|
||||
struct EmailVerifyGateViewModelTests {
|
||||
@Test("resend erfolgreich → .resent")
|
||||
func resendSuccess() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = EmailVerifyGateViewModel(email: "u@x.de", auth: mocked.auth)
|
||||
|
||||
mocked.setHandler { _ in (200, Data(#"{"success":true}"#.utf8)) }
|
||||
|
||||
await model.resend()
|
||||
if case .resent = model.status {
|
||||
#expect(Bool(true))
|
||||
} else {
|
||||
Issue.record("Expected .resent, got \(model.status)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Rate-Limit → .error mit Retry-Hint")
|
||||
func resendRateLimited() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = EmailVerifyGateViewModel(email: "u@x.de", auth: mocked.auth)
|
||||
|
||||
mocked.setHandler { _ in
|
||||
(
|
||||
429,
|
||||
Data(#"{"error":"RATE_LIMITED","retryAfterSec":42,"status":429}"#.utf8),
|
||||
["Retry-After": "42"]
|
||||
)
|
||||
}
|
||||
|
||||
await model.resend()
|
||||
if case let .error(message) = model.status {
|
||||
#expect(message == "Zu viele Versuche. Bitte warte 42s.")
|
||||
} else {
|
||||
Issue.record("Expected .error, got \(model.status)")
|
||||
}
|
||||
}
|
||||
}
|
||||
97
Tests/ManaAuthUITests/ForgotResetViewModelTests.swift
Normal file
97
Tests/ManaAuthUITests/ForgotResetViewModelTests.swift
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import Testing
|
||||
@testable import ManaAuthUI
|
||||
|
||||
@Suite("ForgotPasswordViewModel + ResetPasswordViewModel")
|
||||
@MainActor
|
||||
struct ForgotResetViewModelTests {
|
||||
// MARK: - ForgotPassword
|
||||
|
||||
@Test("forgotPassword erfolgreich → .sent")
|
||||
func forgotSuccess() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = ForgotPasswordViewModel(
|
||||
auth: mocked.auth,
|
||||
resetUniversalLink: URL(string: "https://cardecky.mana.how/auth/reset")!
|
||||
)
|
||||
model.email = "u@x.de"
|
||||
|
||||
let captured = MockURLProtocol.Capture()
|
||||
mocked.setHandler { request in
|
||||
captured.store(request)
|
||||
return (200, Data(#"{"success":true}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
#expect(model.status == .sent)
|
||||
#expect(captured.request?.url?.path == "/api/v1/auth/forgot-password")
|
||||
}
|
||||
|
||||
@Test("forgotPassword leere Email → canSubmit false")
|
||||
func forgotGuards() {
|
||||
let model = ForgotPasswordViewModel(
|
||||
auth: makeMockedAuth().auth,
|
||||
resetUniversalLink: URL(string: "https://x.test/auth/reset")!
|
||||
)
|
||||
#expect(model.canSubmit == false)
|
||||
model.email = "u@x.de"
|
||||
#expect(model.canSubmit == true)
|
||||
}
|
||||
|
||||
// MARK: - ResetPassword
|
||||
|
||||
@Test("resetPassword erfolgreich → .done")
|
||||
func resetSuccess() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = ResetPasswordViewModel(token: "tok123", auth: mocked.auth)
|
||||
model.newPassword = "Neu-987654321"
|
||||
model.confirmPassword = "Neu-987654321"
|
||||
|
||||
mocked.setHandler { _ in (200, Data(#"{"success":true}"#.utf8)) }
|
||||
|
||||
await model.submit()
|
||||
#expect(model.status == .done)
|
||||
#expect(model.newPassword == "")
|
||||
#expect(model.confirmPassword == "")
|
||||
}
|
||||
|
||||
@Test("resetPassword mit abgelaufenem Token → .error")
|
||||
func resetTokenExpired() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = ResetPasswordViewModel(token: "old", auth: mocked.auth)
|
||||
model.newPassword = "Neu-987654321"
|
||||
model.confirmPassword = "Neu-987654321"
|
||||
|
||||
mocked.setHandler { _ in
|
||||
(400, Data(#"{"error":"TOKEN_EXPIRED","status":400}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
if case let .error(message) = model.status {
|
||||
#expect(message == "Der Link ist abgelaufen. Bitte fordere einen neuen an.")
|
||||
} else {
|
||||
Issue.record("Expected .error, got \(model.status)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("resetPassword validationHint bei Mismatch")
|
||||
func resetMismatch() {
|
||||
let model = ResetPasswordViewModel(token: "t", auth: makeMockedAuth().auth)
|
||||
model.newPassword = "lang-genug"
|
||||
model.confirmPassword = "anders-lang"
|
||||
#expect(model.validationHint == "Die Passwörter stimmen nicht überein.")
|
||||
#expect(model.canSubmit == false)
|
||||
}
|
||||
|
||||
@Test("resetPassword canSubmit erfordert ≥8 Zeichen und Match")
|
||||
func resetCanSubmit() {
|
||||
let model = ResetPasswordViewModel(token: "t", auth: makeMockedAuth().auth)
|
||||
model.newPassword = "kurz"
|
||||
model.confirmPassword = "kurz"
|
||||
#expect(model.canSubmit == false)
|
||||
model.newPassword = "lang-genug"
|
||||
model.confirmPassword = "lang-genug"
|
||||
#expect(model.canSubmit == true)
|
||||
}
|
||||
}
|
||||
86
Tests/ManaAuthUITests/LoginViewModelTests.swift
Normal file
86
Tests/ManaAuthUITests/LoginViewModelTests.swift
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import Testing
|
||||
@testable import ManaAuthUI
|
||||
|
||||
@Suite("LoginViewModel")
|
||||
@MainActor
|
||||
struct LoginViewModelTests {
|
||||
@Test("Erfolgreicher Login setzt status auf .idle und cleared das Passwort")
|
||||
func successfulLogin() async throws {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = LoginViewModel(auth: mocked.auth)
|
||||
model.email = "u@x.de"
|
||||
model.password = "Aa-123456789"
|
||||
|
||||
let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig"
|
||||
mocked.setHandler { _ in
|
||||
(200, Data(#"{"accessToken":"\#(access)","refreshToken":"r"}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
#expect(model.status == .idle)
|
||||
#expect(model.password == "")
|
||||
#expect(mocked.auth.status == .signedIn(email: "u@x.de"))
|
||||
}
|
||||
|
||||
@Test("EMAIL_NOT_VERIFIED schaltet ViewModel auf .emailNotVerified")
|
||||
func emailNotVerifiedPath() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = LoginViewModel(auth: mocked.auth)
|
||||
model.email = "u@x.de"
|
||||
model.password = "pw"
|
||||
|
||||
mocked.setHandler { _ in
|
||||
(403, Data(#"{"error":"EMAIL_NOT_VERIFIED","status":403}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
if case let .emailNotVerified(email) = model.status {
|
||||
#expect(email == "u@x.de")
|
||||
} else {
|
||||
Issue.record("Expected .emailNotVerified, got \(model.status)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Invalid-Credentials liefert .error mit deutscher Nachricht")
|
||||
func invalidCredentials() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = LoginViewModel(auth: mocked.auth)
|
||||
model.email = "u@x.de"
|
||||
model.password = "wrong"
|
||||
|
||||
mocked.setHandler { _ in
|
||||
(401, Data(#"{"error":"INVALID_CREDENTIALS","status":401}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
if case let .error(message) = model.status {
|
||||
#expect(message == "Email oder Passwort falsch")
|
||||
} else {
|
||||
Issue.record("Expected .error, got \(model.status)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("canSubmit ist false bei leeren Feldern")
|
||||
func canSubmitGuards() {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = LoginViewModel(auth: mocked.auth)
|
||||
#expect(model.canSubmit == false)
|
||||
model.email = "u@x.de"
|
||||
#expect(model.canSubmit == false)
|
||||
model.password = "pw"
|
||||
#expect(model.canSubmit == true)
|
||||
}
|
||||
|
||||
@Test("resetToIdle bringt Status zurück auf idle")
|
||||
func resetToIdleClearsState() {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = LoginViewModel(auth: mocked.auth)
|
||||
model.email = "u@x.de"
|
||||
model.password = "pw"
|
||||
model.resetToIdle()
|
||||
#expect(model.status == .idle)
|
||||
#expect(model.password == "")
|
||||
}
|
||||
}
|
||||
35
Tests/ManaAuthUITests/ManaBrandConfigTests.swift
Normal file
35
Tests/ManaAuthUITests/ManaBrandConfigTests.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import SwiftUI
|
||||
import Testing
|
||||
@testable import ManaAuthUI
|
||||
|
||||
@Suite("ManaBrandConfig")
|
||||
struct ManaBrandConfigTests {
|
||||
@Test("systemDefault setzt sinnvolle Defaults")
|
||||
func systemDefaultDefaults() {
|
||||
let config = ManaBrandConfig.systemDefault
|
||||
#expect(config.appName == "mana")
|
||||
#expect(config.tagline == nil)
|
||||
#expect(config.logoSymbol == nil)
|
||||
}
|
||||
|
||||
@Test("Apps können ein eigenes Brand-Config bauen")
|
||||
func customConfig() {
|
||||
let cardecky = ManaBrandConfig(
|
||||
appName: "Cardecky",
|
||||
tagline: "Karteikarten des Vereins mana e.V.",
|
||||
logoSymbol: "rectangle.stack.fill",
|
||||
background: .white,
|
||||
foreground: .black,
|
||||
surface: .gray,
|
||||
mutedForeground: .gray,
|
||||
border: .gray,
|
||||
primary: .green,
|
||||
primaryForeground: .white,
|
||||
error: .red,
|
||||
success: .green
|
||||
)
|
||||
#expect(cardecky.appName == "Cardecky")
|
||||
#expect(cardecky.tagline == "Karteikarten des Vereins mana e.V.")
|
||||
#expect(cardecky.logoSymbol == "rectangle.stack.fill")
|
||||
}
|
||||
}
|
||||
107
Tests/ManaAuthUITests/MockURLProtocol.swift
Normal file
107
Tests/ManaAuthUITests/MockURLProtocol.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
|
||||
/// URLProtocol-Mock mit Pro-Test-Routing: jede Test-AuthClient-
|
||||
/// Instanz kriegt eine eigene Test-ID als HTTP-Header, der Mock
|
||||
/// routet nach ID zum richtigen Handler. Das löst die Parallel-
|
||||
/// Pollution zwischen Test-Suites (mehrere `.serialized`-Suites
|
||||
/// laufen untereinander parallel, der globale Handler-Slot wäre
|
||||
/// sonst ein Race).
|
||||
final class MockURLProtocol: URLProtocol, @unchecked Sendable {
|
||||
typealias Handler = @Sendable (URLRequest) -> Any
|
||||
|
||||
nonisolated(unsafe) static var handlersStorage: [String: Handler] = [:]
|
||||
static let handlersLock = NSLock()
|
||||
|
||||
static func register(testID: String, handler: @escaping Handler) {
|
||||
handlersLock.lock(); defer { handlersLock.unlock() }
|
||||
handlersStorage[testID] = handler
|
||||
}
|
||||
|
||||
static func unregister(testID: String) {
|
||||
handlersLock.lock(); defer { handlersLock.unlock() }
|
||||
handlersStorage.removeValue(forKey: testID)
|
||||
}
|
||||
|
||||
private static func lookup(testID: String) -> Handler? {
|
||||
handlersLock.lock(); defer { handlersLock.unlock() }
|
||||
return handlersStorage[testID]
|
||||
}
|
||||
|
||||
final class Capture: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var stored: URLRequest?
|
||||
|
||||
func store(_ r: URLRequest) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stored = r
|
||||
}
|
||||
|
||||
var request: URLRequest? {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
return stored
|
||||
}
|
||||
}
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool { true }
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
||||
override func stopLoading() {}
|
||||
|
||||
override func startLoading() {
|
||||
let testID = request.value(forHTTPHeaderField: "X-Test-ID") ?? ""
|
||||
guard let handler = MockURLProtocol.lookup(testID: testID) else {
|
||||
client?.urlProtocol(self, didFailWithError: URLError(.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
let result = handler(request)
|
||||
let status: Int
|
||||
let body: Data
|
||||
let headers: [String: String]
|
||||
if let tuple = result as? (Int, Data, [String: String]) {
|
||||
status = tuple.0; body = tuple.1; headers = tuple.2
|
||||
} else if let tuple = result as? (Int, Data) {
|
||||
status = tuple.0; body = tuple.1; headers = [:]
|
||||
} else {
|
||||
client?.urlProtocol(self, didFailWithError: URLError(.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
let response = HTTPURLResponse(
|
||||
url: request.url!,
|
||||
statusCode: status,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: headers
|
||||
)!
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
client?.urlProtocol(self, didLoad: body)
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bündelt einen `AuthClient` mit seiner Test-ID. Tests setzen den
|
||||
/// Handler über die Test-ID statt über einen globalen Slot.
|
||||
@MainActor
|
||||
struct MockedAuth {
|
||||
let auth: AuthClient
|
||||
let testID: String
|
||||
|
||||
func setHandler(_ handler: @escaping MockURLProtocol.Handler) {
|
||||
MockURLProtocol.register(testID: testID, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func makeMockedAuth() -> MockedAuth {
|
||||
let testID = UUID().uuidString
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
configuration.protocolClasses = [MockURLProtocol.self]
|
||||
configuration.httpAdditionalHeaders = ["X-Test-ID": testID]
|
||||
let session = URLSession(configuration: configuration)
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.test")!,
|
||||
keychainService: "ev.mana.test.\(testID)",
|
||||
keychainAccessGroup: nil
|
||||
)
|
||||
return MockedAuth(auth: AuthClient(config: config, session: session), testID: testID)
|
||||
}
|
||||
96
Tests/ManaAuthUITests/SignUpViewModelTests.swift
Normal file
96
Tests/ManaAuthUITests/SignUpViewModelTests.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import Testing
|
||||
@testable import ManaAuthUI
|
||||
|
||||
@Suite("SignUpViewModel")
|
||||
@MainActor
|
||||
struct SignUpViewModelTests {
|
||||
@Test("Registrierung ohne Tokens → awaitingVerification")
|
||||
func awaitsVerification() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = SignUpViewModel(auth: mocked.auth)
|
||||
model.email = "new@x.de"
|
||||
model.password = "Aa-123456789"
|
||||
|
||||
mocked.setHandler { _ in
|
||||
(200, Data(#"{"user":{"id":"u1","email":"new@x.de"}}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
if case let .awaitingVerification(email) = model.status {
|
||||
#expect(email == "new@x.de")
|
||||
} else {
|
||||
Issue.record("Expected .awaitingVerification, got \(model.status)")
|
||||
}
|
||||
#expect(model.password == "")
|
||||
}
|
||||
|
||||
@Test("Registrierung mit Tokens → signedIn")
|
||||
func registersAndSignsIn() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = SignUpViewModel(auth: mocked.auth)
|
||||
model.email = "new@x.de"
|
||||
model.password = "Aa-123456789"
|
||||
|
||||
let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig"
|
||||
mocked.setHandler { _ in
|
||||
(200, Data(#"""
|
||||
{"user":{"id":"u1","email":"new@x.de"},"accessToken":"\#(access)","refreshToken":"r"}
|
||||
"""#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
#expect(model.status == .signedIn)
|
||||
}
|
||||
|
||||
@Test("Email-Konflikt liefert lokalisierte Fehlermeldung")
|
||||
func emailConflict() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = SignUpViewModel(auth: mocked.auth)
|
||||
model.email = "old@x.de"
|
||||
model.password = "Aa-123456789"
|
||||
|
||||
mocked.setHandler { _ in
|
||||
(409, Data(#"{"error":"EMAIL_ALREADY_REGISTERED","status":409}"#.utf8))
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
if case let .error(message) = model.status {
|
||||
#expect(message == "Diese Email ist bereits registriert.")
|
||||
} else {
|
||||
Issue.record("Expected .error, got \(model.status)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("passwordHint warnt bei zu kurzem Passwort")
|
||||
func passwordHint() {
|
||||
let model = SignUpViewModel(auth: makeMockedAuth().auth)
|
||||
model.password = ""
|
||||
#expect(model.passwordHint == nil)
|
||||
model.password = "kurz"
|
||||
#expect(model.passwordHint == "Passwort muss mindestens 8 Zeichen lang sein.")
|
||||
model.password = "lang-genug"
|
||||
#expect(model.passwordHint == nil)
|
||||
}
|
||||
|
||||
@Test("submit mit zu kurzem Passwort macht keinen Server-Call")
|
||||
func submitGuardsShortPassword() async {
|
||||
let mocked = makeMockedAuth()
|
||||
let model = SignUpViewModel(auth: mocked.auth)
|
||||
model.email = "u@x.de"
|
||||
model.password = "kurz"
|
||||
|
||||
mocked.setHandler { _ in
|
||||
Issue.record("Server darf nicht aufgerufen werden")
|
||||
return (500, Data())
|
||||
}
|
||||
|
||||
await model.submit()
|
||||
if case let .error(message) = model.status {
|
||||
#expect(message == "Passwort muss mindestens 8 Zeichen lang sein.")
|
||||
} else {
|
||||
Issue.record("Expected .error, got \(model.status)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue