Adopt root-level tsswift app layout
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.
This commit is contained in:
2026-04-19 01:21:43 +02:00
parent d534964601
commit a6939453f8
61 changed files with 2341 additions and 3 deletions
+249
View File
@@ -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 {}
}