Some checks failed
CI / test (push) Has been cancelled
Move the app payload under swift/ while keeping git, package.json, and .smartconfig.json at the repo root. This standardizes the Swift app setup so build, test, run, and watch workflows match the other repos.
250 lines
8.5 KiB
Swift
250 lines
8.5 KiB
Swift
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 {}
|
|
}
|