移除 SplashCoordinator,bootstrap 期间用 SplashView 覆盖避免空白闪屏,其余账号数据改为后台补充刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
145 lines
5.5 KiB
Swift
145 lines
5.5 KiB
Swift
//
|
||
// 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<StoreItem> {
|
||
ListPayload(total: 0, list: [])
|
||
}
|
||
|
||
func scenicSpotListAll(scenicId: Int) async throws -> ListPayload<ScenicSpotItem> {
|
||
ListPayload(total: 0, list: [])
|
||
}
|
||
}
|