// // SessionBootstrapperTests.swift // suixinkanTests // // Created by Codex on 2026/6/30. // import XCTest @testable import suixinkan @MainActor /// 冷启动恢复测试,覆盖阻塞权限恢复与后台补充刷新。 final class SessionBootstrapperTests: XCTestCase { /// 测试无 token 时直接进入未登录状态。 func testRestoreWithoutTokenLogsOut() async { let defaults = makeDefaults() let tokenStore = SessionTokenStore(defaults: defaults, key: "test.token") let snapshotStore = AccountSnapshotStore(defaults: defaults, key: "test.snapshot") let bootstrapper = SessionBootstrapper(tokenStore: tokenStore, snapshotStore: snapshotStore) let appSession = AppSession() let accountContext = AccountContext() let permissionContext = PermissionContext() let toastCenter = ToastCenter() await bootstrapper.restore( appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, profileAPI: MockUserProfileAPI(), accountContextAPI: MockAccountContextAPI(), toastCenter: toastCenter ) XCTAssertEqual(appSession.phase, .loggedOut) } /// 测试有 token 时阻塞等待 rolePermissions 后立即进入 loggedIn。 func testRestoreMarksLoggedInAfterRolePermissions() async { let defaults = makeDefaults() let tokenStore = SessionTokenStore(defaults: defaults, key: "test.token") try? tokenStore.save("token-1") let snapshotStore = AccountSnapshotStore(defaults: defaults, key: "test.snapshot") let bootstrapper = SessionBootstrapper(tokenStore: tokenStore, snapshotStore: snapshotStore) let appSession = AppSession() let accountContext = AccountContext() let permissionContext = PermissionContext() let toastCenter = ToastCenter() let api = MockAccountContextAPI() api.roles = [ RolePermissionResponse( role: RoleInfo(id: 7, name: "运营", permission: [PermissionItem(id: 1, name: "首页", uri: "/home")]), scenic: [ScenicInfo(id: 88, name: "东湖景区")] ) ] await bootstrapper.restore( appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, profileAPI: MockUserProfileAPI(), accountContextAPI: api, toastCenter: toastCenter ) XCTAssertEqual(appSession.phase, .loggedIn) XCTAssertEqual(permissionContext.currentRole?.id, 7) XCTAssertTrue(permissionContext.canAccess("/home")) let profileExpectation = expectation(description: "profile loaded in background") DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { if accountContext.profile?.displayName == "测试用户" { profileExpectation.fulfill() } } await fulfillment(of: [profileExpectation], timeout: 2) } /// 测试 rolePermissions 鉴权失败时会清空本地登录态。 func testRestoreLogsOutWhenRolePermissionsExpired() async { let defaults = makeDefaults() let tokenStore = SessionTokenStore(defaults: defaults, key: "test.token") try? tokenStore.save("token-1") let snapshotStore = AccountSnapshotStore(defaults: defaults, key: "test.snapshot") snapshotStore.save(AccountSnapshot(profile: AccountProfile(userId: "1", displayName: "本地用户"))) let bootstrapper = SessionBootstrapper(tokenStore: tokenStore, snapshotStore: snapshotStore) let appSession = AppSession() let accountContext = AccountContext() let permissionContext = PermissionContext() let toastCenter = ToastCenter() let api = MockAccountContextAPI() api.roleError = APIError.httpStatus(401, "登录状态已失效") await bootstrapper.restore( appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, profileAPI: MockUserProfileAPI(), accountContextAPI: api, toastCenter: toastCenter ) XCTAssertEqual(appSession.phase, .loggedOut) XCTAssertNil(tokenStore.load()) XCTAssertNil(snapshotStore.load()) } private func makeDefaults() -> UserDefaults { let suiteName = "SessionBootstrapperTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) return defaults } } @MainActor private final class MockUserProfileAPI: UserProfileServing { func userInfo() async throws -> UserInfoResponse { try await Task.sleep(nanoseconds: 100_000_000) return UserInfoResponse(nickname: "测试用户", roleName: "运营") } } @MainActor private final class MockAccountContextAPI: AccountContextServing { var roles: [RolePermissionResponse] = [] var roleError: Error? func rolePermissions() async throws -> [RolePermissionResponse] { if let roleError { throw roleError } return roles } func scenicListAll() async throws -> ScenicListAllResponse { ScenicListAllResponse(total: 0, list: []) } func storeAll() async throws -> ListPayload { ListPayload(total: 0, list: []) } func scenicSpotListAll(scenicId: Int) async throws -> ListPayload { ListPayload(total: 0, list: []) } }