import Foundation import ManaCore import Testing @testable import ManaAuthUI @Suite("ManaAuthGate") @MainActor struct ManaAuthGateTests { @Test("require mit .signedIn führt Action sofort aus, kein Sheet") func runsImmediatelyWhenSignedIn() async throws { let mocked = makeMockedAuth() await signInMockedAuth(mocked) let gate = ManaAuthGate(auth: mocked.auth) var didRun = false gate.require { didRun = true } #expect(didRun) #expect(!gate.isPresentingSignIn) } @Test("require mit .signedOut öffnet Sheet, Action wartet") func defersWhenSignedOut() { let mocked = makeMockedAuth() mocked.auth.bootstrap() // → .signedOut let gate = ManaAuthGate(auth: mocked.auth) var didRun = false gate.require { didRun = true } #expect(!didRun) #expect(gate.isPresentingSignIn) } @Test("require mit .guest öffnet Sheet, Action wartet") func defersWhenGuest() throws { let mocked = makeMockedAuth() _ = try mocked.auth.enterGuestMode() let gate = ManaAuthGate(auth: mocked.auth) var didRun = false gate.require { didRun = true } #expect(!didRun) #expect(gate.isPresentingSignIn) } @Test("resolvePending läuft, sobald Status auf .signedIn wechselt") func resolvesPendingAfterSignIn() async throws { let mocked = makeMockedAuth() mocked.auth.bootstrap() let gate = ManaAuthGate(auth: mocked.auth) var didRun = false gate.require { didRun = true } #expect(!didRun) #expect(gate.isPresentingSignIn) await signInMockedAuth(mocked) gate.resolvePending() #expect(didRun) #expect(!gate.isPresentingSignIn) } @Test("resolvePending ist no-op wenn noch nicht signedIn") func resolvePendingNoOpWhenNotSignedIn() { let mocked = makeMockedAuth() mocked.auth.bootstrap() let gate = ManaAuthGate(auth: mocked.auth) var didRun = false gate.require { didRun = true } gate.resolvePending() #expect(!didRun) #expect(gate.isPresentingSignIn) } @Test("cancelPending verwirft Action — danach kein Lauf bei resolvePending") func cancelDiscardsPending() async throws { let mocked = makeMockedAuth() mocked.auth.bootstrap() let gate = ManaAuthGate(auth: mocked.auth) var didRun = false gate.require { didRun = true } gate.cancelPending() await signInMockedAuth(mocked) gate.resolvePending() #expect(!didRun) } @Test("lastReason wird gesetzt") func lastReasonIsRecorded() { let mocked = makeMockedAuth() mocked.auth.bootstrap() let gate = ManaAuthGate(auth: mocked.auth) gate.require(reason: "ai-generate") {} #expect(gate.lastReason == "ai-generate") } // Die async-Overload (`Task { await action() }`) ist trivial und // ein dedizierter Test über `Task.sleep` ist timing-fragil. // Die sync-Variante prüft die State-Maschine vollständig; die // async-Variante teilt sich Pending/Resolve-Logik mit der sync- // Variante (siehe ManaAuthGate.swift). } /// Loggt den `MockedAuth` über den echten signIn-Pfad ein. Wird genutzt /// statt direktem `persistSession`, weil letzteres `internal` zu ManaCore /// ist und aus den ui-Tests nicht erreichbar. @MainActor func signInMockedAuth(_ mocked: MockedAuth, email: String = "u@x.de") async { // Gültiger HS256-Header.payload (exp 2_000_000_000) — JWT.expiry() // läuft nicht in den Refresh-Pfad. let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig" mocked.setHandler { _ in (200, Data(#"{"accessToken":"\#(access)","refreshToken":"r"}"#.utf8)) } await mocked.auth.signIn(email: email, password: "Aa-123456789") }