This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import XCTest
|
||||
@testable import IDPGlobal
|
||||
|
||||
@MainActor
|
||||
final class AppViewModelTests: XCTestCase {
|
||||
func testBootstrapRestoresPersistedState() async {
|
||||
let session = makeSession()
|
||||
let profile = makeProfile()
|
||||
let snapshot = makeSnapshot(profile: profile)
|
||||
let store = InMemoryAppStateStore(
|
||||
state: PersistedAppState(
|
||||
session: session,
|
||||
profile: profile,
|
||||
requests: snapshot.requests,
|
||||
notifications: snapshot.notifications
|
||||
)
|
||||
)
|
||||
let service = StubService(
|
||||
bootstrapContext: BootstrapContext(suggestedPairingPayload: "idp.global://pair?token=fresh-token&origin=code.foss.global&device=Fresh%20Browser"),
|
||||
signInResult: SignInResult(session: session, snapshot: snapshot),
|
||||
dashboardSnapshot: snapshot
|
||||
)
|
||||
let coordinator = StubNotificationCoordinator(status: .allowed)
|
||||
let model = AppViewModel(
|
||||
service: service,
|
||||
notificationCoordinator: coordinator,
|
||||
appStateStore: store,
|
||||
launchArguments: []
|
||||
)
|
||||
|
||||
await model.bootstrap()
|
||||
|
||||
XCTAssertEqual(model.session, session)
|
||||
XCTAssertEqual(model.profile, profile)
|
||||
XCTAssertEqual(model.requests.map(\.id), snapshot.requests.sorted { $0.createdAt > $1.createdAt }.map(\.id))
|
||||
XCTAssertEqual(model.notifications.map(\.id), snapshot.notifications.sorted { $0.sentAt > $1.sentAt }.map(\.id))
|
||||
XCTAssertEqual(model.manualPairingPayload, session.pairingCode)
|
||||
XCTAssertEqual(model.suggestedPairingPayload, "idp.global://pair?token=fresh-token&origin=code.foss.global&device=Fresh%20Browser")
|
||||
XCTAssertEqual(model.notificationPermission, .allowed)
|
||||
}
|
||||
|
||||
func testSignInPersistsAuthenticatedState() async {
|
||||
let session = makeSession()
|
||||
let profile = makeProfile()
|
||||
let snapshot = makeSnapshot(profile: profile)
|
||||
let store = InMemoryAppStateStore()
|
||||
let service = StubService(
|
||||
bootstrapContext: BootstrapContext(suggestedPairingPayload: session.pairingCode),
|
||||
signInResult: SignInResult(session: session, snapshot: snapshot),
|
||||
dashboardSnapshot: snapshot
|
||||
)
|
||||
let model = AppViewModel(
|
||||
service: service,
|
||||
notificationCoordinator: StubNotificationCoordinator(status: .allowed),
|
||||
appStateStore: store,
|
||||
launchArguments: []
|
||||
)
|
||||
|
||||
await model.signIn(with: session.pairingCode, transport: .preview)
|
||||
|
||||
XCTAssertEqual(model.session, session)
|
||||
XCTAssertEqual(store.storedState?.session, session)
|
||||
XCTAssertEqual(store.storedState?.profile, profile)
|
||||
XCTAssertEqual(store.storedState?.requests.map(\.id), snapshot.requests.sorted { $0.createdAt > $1.createdAt }.map(\.id))
|
||||
XCTAssertEqual(store.storedState?.notifications.map(\.id), snapshot.notifications.sorted { $0.sentAt > $1.sentAt }.map(\.id))
|
||||
}
|
||||
|
||||
func testSignOutClearsPersistedState() async {
|
||||
let session = makeSession()
|
||||
let profile = makeProfile()
|
||||
let snapshot = makeSnapshot(profile: profile)
|
||||
let store = InMemoryAppStateStore(
|
||||
state: PersistedAppState(
|
||||
session: session,
|
||||
profile: profile,
|
||||
requests: snapshot.requests,
|
||||
notifications: snapshot.notifications
|
||||
)
|
||||
)
|
||||
let model = AppViewModel(
|
||||
service: StubService(
|
||||
bootstrapContext: BootstrapContext(suggestedPairingPayload: session.pairingCode),
|
||||
signInResult: SignInResult(session: session, snapshot: snapshot),
|
||||
dashboardSnapshot: snapshot
|
||||
),
|
||||
notificationCoordinator: StubNotificationCoordinator(status: .allowed),
|
||||
appStateStore: store,
|
||||
launchArguments: []
|
||||
)
|
||||
|
||||
await model.bootstrap()
|
||||
model.signOut()
|
||||
|
||||
XCTAssertNil(model.session)
|
||||
XCTAssertNil(model.profile)
|
||||
XCTAssertTrue(store.didClear)
|
||||
XCTAssertNil(store.storedState)
|
||||
}
|
||||
|
||||
private func makeSession() -> AuthSession {
|
||||
AuthSession(
|
||||
deviceName: "Safari on Berlin MBP",
|
||||
originHost: "code.foss.global",
|
||||
pairedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
||||
tokenPreview: "berlin",
|
||||
pairingCode: "idp.global://pair?token=swiftapp-demo-berlin&origin=code.foss.global&device=Safari%20on%20Berlin%20MBP",
|
||||
pairingTransport: .preview
|
||||
)
|
||||
}
|
||||
|
||||
private func makeProfile() -> MemberProfile {
|
||||
MemberProfile(
|
||||
name: "Phil Kunz",
|
||||
handle: "phil@idp.global",
|
||||
organization: "idp.global",
|
||||
deviceCount: 4,
|
||||
recoverySummary: "Recovery kit healthy with 2 of 3 backup paths verified."
|
||||
)
|
||||
}
|
||||
|
||||
private func makeSnapshot(profile: MemberProfile) -> DashboardSnapshot {
|
||||
DashboardSnapshot(
|
||||
profile: profile,
|
||||
requests: [
|
||||
ApprovalRequest(
|
||||
title: "Later request",
|
||||
subtitle: "Newer",
|
||||
source: "later.idp.global",
|
||||
createdAt: Date(timeIntervalSince1970: 200),
|
||||
kind: .signIn,
|
||||
risk: .routine,
|
||||
scopes: ["proof:basic"],
|
||||
status: .pending
|
||||
),
|
||||
ApprovalRequest(
|
||||
title: "Earlier request",
|
||||
subtitle: "Older",
|
||||
source: "earlier.idp.global",
|
||||
createdAt: Date(timeIntervalSince1970: 100),
|
||||
kind: .elevatedAction,
|
||||
risk: .elevated,
|
||||
scopes: ["proof:high"],
|
||||
status: .approved
|
||||
)
|
||||
],
|
||||
notifications: [
|
||||
AppNotification(
|
||||
title: "Older notification",
|
||||
message: "Oldest",
|
||||
sentAt: Date(timeIntervalSince1970: 100),
|
||||
kind: .system,
|
||||
isUnread: false
|
||||
),
|
||||
AppNotification(
|
||||
title: "Newer notification",
|
||||
message: "Newest",
|
||||
sentAt: Date(timeIntervalSince1970: 200),
|
||||
kind: .security,
|
||||
isUnread: true
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class InMemoryAppStateStore: AppStateStoring {
|
||||
var storedState: PersistedAppState?
|
||||
var didClear = false
|
||||
|
||||
init(state: PersistedAppState? = nil) {
|
||||
storedState = state
|
||||
}
|
||||
|
||||
func load() -> PersistedAppState? {
|
||||
storedState
|
||||
}
|
||||
|
||||
func save(_ state: PersistedAppState) {
|
||||
storedState = state
|
||||
didClear = false
|
||||
}
|
||||
|
||||
func clear() {
|
||||
storedState = nil
|
||||
didClear = true
|
||||
}
|
||||
}
|
||||
|
||||
private actor StubService: IDPServicing {
|
||||
private let bootstrapContext: BootstrapContext
|
||||
private let signInResult: SignInResult
|
||||
private let dashboardSnapshot: DashboardSnapshot
|
||||
|
||||
init(bootstrapContext: BootstrapContext, signInResult: SignInResult, dashboardSnapshot: DashboardSnapshot) {
|
||||
self.bootstrapContext = bootstrapContext
|
||||
self.signInResult = signInResult
|
||||
self.dashboardSnapshot = dashboardSnapshot
|
||||
}
|
||||
|
||||
func bootstrap() async throws -> BootstrapContext {
|
||||
bootstrapContext
|
||||
}
|
||||
|
||||
func signIn(with request: PairingAuthenticationRequest) async throws -> SignInResult {
|
||||
signInResult
|
||||
}
|
||||
|
||||
func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot {
|
||||
dashboardSnapshot
|
||||
}
|
||||
|
||||
func refreshDashboard() async throws -> DashboardSnapshot {
|
||||
dashboardSnapshot
|
||||
}
|
||||
|
||||
func approveRequest(id: UUID) async throws -> DashboardSnapshot {
|
||||
dashboardSnapshot
|
||||
}
|
||||
|
||||
func rejectRequest(id: UUID) async throws -> DashboardSnapshot {
|
||||
dashboardSnapshot
|
||||
}
|
||||
|
||||
func simulateIncomingRequest() async throws -> DashboardSnapshot {
|
||||
dashboardSnapshot
|
||||
}
|
||||
|
||||
func markNotificationRead(id: UUID) async throws -> DashboardSnapshot {
|
||||
dashboardSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
private final class StubNotificationCoordinator: NotificationCoordinating {
|
||||
private let status: NotificationPermissionState
|
||||
|
||||
init(status: NotificationPermissionState) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
func authorizationStatus() async -> NotificationPermissionState {
|
||||
status
|
||||
}
|
||||
|
||||
func requestAuthorization() async throws -> NotificationPermissionState {
|
||||
status
|
||||
}
|
||||
|
||||
func scheduleTestNotification(title: String, body: String) async throws {}
|
||||
}
|
||||
Reference in New Issue
Block a user