Initial commit

This commit is contained in:
2026-06-22 11:28:01 +08:00
commit ace9c94359
84 changed files with 8899 additions and 0 deletions

View File

@ -0,0 +1,29 @@
//
// APIErrorTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/20.
//
import XCTest
@testable import suixinkan
///
final class APIErrorTests: XCTestCase {
/// HTTP 401 403
func testAuthenticationExpiredForUnauthorizedHTTPStatus() {
XCTAssertTrue(APIError.isAuthenticationExpired(APIError.httpStatus(401, "Unauthorized")))
XCTAssertTrue(APIError.isAuthenticationExpired(APIError.httpStatus(403, "Forbidden")))
}
///
func testAuthenticationExpiredForServerTokenMessages() {
XCTAssertTrue(APIError.isAuthenticationExpired(APIError.serverCode(200001, "token 已过期")))
XCTAssertTrue(APIError.isAuthenticationExpired(APIError.serverCode(0, "登录失效,请重新登录")))
}
///
func testNetworkErrorIsNotAuthenticationExpired() {
XCTAssertFalse(APIError.isAuthenticationExpired(APIError.networkFailed("请求超时,请稍后重试")))
}
}

View File

@ -0,0 +1,137 @@
//
// AccountContextLoaderTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class AccountContextLoaderTests: XCTestCase {
///
func testRefreshLoadsRolePermissionScenicAndStoreContext() async throws {
let accountContext = AccountContext()
let permissionContext = PermissionContext()
let api = MockAccountContextAPI()
api.roles = [
RolePermissionResponse(
role: RoleInfo(id: 7, name: "运营", permission: [PermissionItem(id: 1, name: "首页", uri: "/home")]),
scenic: [ScenicInfo(id: 88, name: "东湖景区")]
)
]
api.scenicResponse = ScenicListAllResponse(total: 1, list: [ScenicListItem(id: 88, name: "东湖景区")])
api.storeResponse = ListPayload(total: 1, list: [StoreItem(id: 66, scenicId: 88, name: "东湖门店")])
try await AccountContextLoader().refresh(
accountContext: accountContext,
permissionContext: permissionContext,
profileAPI: MockUserProfileAPI(),
accountContextAPI: api
)
XCTAssertEqual(accountContext.profile?.displayName, "测试用户")
XCTAssertEqual(permissionContext.currentRole?.id, 7)
XCTAssertTrue(permissionContext.canAccess("/home"))
XCTAssertEqual(accountContext.currentScenic?.id, 88)
XCTAssertEqual(accountContext.currentStore?.id, 66)
}
///
func testRefreshFallsBackToRoleScenicsWhenScenicAPIThrows() async throws {
let accountContext = AccountContext()
let permissionContext = PermissionContext()
let api = MockAccountContextAPI()
api.roles = [
RolePermissionResponse(
role: RoleInfo(id: 7, name: "运营"),
scenic: [
ScenicInfo(id: 88, name: "东湖景区"),
ScenicInfo(id: 88, name: "东湖景区")
]
)
]
api.scenicError = APIError.networkFailed("景区接口失败")
try await AccountContextLoader().refresh(
accountContext: accountContext,
permissionContext: permissionContext,
profileAPI: MockUserProfileAPI(),
accountContextAPI: api
)
XCTAssertEqual(accountContext.scenicScopes.map(\.id), [88])
}
///
func testRefreshAllowsStoreFailure() async throws {
let accountContext = AccountContext()
let permissionContext = PermissionContext()
let api = MockAccountContextAPI()
api.roles = [
RolePermissionResponse(
role: RoleInfo(id: 7, name: "运营"),
scenic: [ScenicInfo(id: 88, name: "东湖景区")]
)
]
api.storeError = APIError.networkFailed("门店接口失败")
try await AccountContextLoader().refresh(
accountContext: accountContext,
permissionContext: permissionContext,
profileAPI: MockUserProfileAPI(),
accountContextAPI: api
)
XCTAssertEqual(accountContext.currentScenic?.id, 88)
XCTAssertTrue(accountContext.storeScopes.isEmpty)
XCTAssertNil(accountContext.currentStore)
}
}
@MainActor
///
private final class MockUserProfileAPI: UserProfileServing {
///
func userInfo() async throws -> UserInfoResponse {
UserInfoResponse(nickname: "测试用户", roleName: "运营")
}
}
@MainActor
///
private final class MockAccountContextAPI: AccountContextServing {
var roles: [RolePermissionResponse] = []
var scenicResponse = ScenicListAllResponse(total: 0, list: [])
var storeResponse = ListPayload(total: 0, list: [StoreItem]())
var scenicError: Error?
var storeError: Error?
///
func rolePermissions() async throws -> [RolePermissionResponse] {
roles
}
///
func scenicListAll() async throws -> ScenicListAllResponse {
if let scenicError {
throw scenicError
}
return scenicResponse
}
///
func storeAll() async throws -> ListPayload<StoreItem> {
if let storeError {
throw storeError
}
return storeResponse
}
///
func scenicSpotListAll(scenicId: Int) async throws -> ListPayload<ScenicSpotItem> {
ListPayload(total: 0, list: [])
}
}

View File

@ -0,0 +1,133 @@
//
// AccountContextTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/20.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class AccountContextTests: XCTestCase {
/// replaceScopes ID
func testReplaceScopesRestoresCurrentSelectionsById() {
let context = AccountContext()
let scenicScopes = [
BusinessScope(id: 1, name: "默认景区", kind: .scenic),
BusinessScope(id: 2, name: "缓存景区", kind: .scenic)
]
let storeScopes = [
BusinessScope(id: 10, name: "默认门店", kind: .store),
BusinessScope(id: 20, name: "缓存门店", kind: .store)
]
context.replaceScopes(
scenic: scenicScopes,
stores: storeScopes,
currentScenicId: 2,
currentStoreId: 20
)
XCTAssertEqual(context.currentScenic?.id, 2)
XCTAssertEqual(context.currentStore?.id, 20)
}
/// ID 退
func testReplaceScopesFallsBackToFirstScopeWhenCachedIdIsMissing() {
let context = AccountContext()
context.replaceScopes(
scenic: [BusinessScope(id: 1, name: "默认景区", kind: .scenic)],
stores: [BusinessScope(id: 10, name: "默认门店", kind: .store)],
currentScenicId: 999,
currentStoreId: 999
)
XCTAssertEqual(context.currentScenic?.id, 1)
XCTAssertEqual(context.currentStore?.id, 10)
}
///
func testSelectScenicResolvesStoreWithinSelectedScenic() {
let context = AccountContext()
context.replaceScopes(
scenic: [
BusinessScope(id: 1, name: "西湖景区", kind: .scenic),
BusinessScope(id: 2, name: "东湖景区", kind: .scenic)
],
stores: [
BusinessScope(id: 10, name: "西湖门店", kind: .store, parentScenicId: 1),
BusinessScope(id: 20, name: "东湖门店", kind: .store, parentScenicId: 2)
],
currentScenicId: 1,
currentStoreId: 10
)
context.selectScenic(id: 2)
XCTAssertEqual(context.currentScenic?.id, 2)
XCTAssertEqual(context.currentStore?.id, 20)
}
///
func testSelectRoleUpdatesScenicScopesAndStoreSelection() {
let accountContext = AccountContext()
let permissionContext = PermissionContext()
accountContext.replaceScopes(
scenic: [BusinessScope(id: 1, name: "旧景区", kind: .scenic)],
stores: [
BusinessScope(id: 10, name: "旧门店", kind: .store, parentScenicId: 1),
BusinessScope(id: 20, name: "新门店", kind: .store, parentScenicId: 2)
],
currentScenicId: 1,
currentStoreId: 10
)
permissionContext.replaceRolePermissions([
RolePermissionResponse(
role: RoleInfo(id: 1, name: "旧角色"),
scenic: [ScenicInfo(id: 1, name: "旧景区")]
),
RolePermissionResponse(
role: RoleInfo(id: 2, name: "新角色"),
scenic: [ScenicInfo(id: 2, name: "新景区")]
)
])
permissionContext.selectRole(id: 2, accountContext: accountContext)
XCTAssertEqual(permissionContext.currentRole?.id, 2)
XCTAssertEqual(accountContext.scenicScopes.map(\.id), [2])
XCTAssertEqual(accountContext.currentScenic?.id, 2)
XCTAssertEqual(accountContext.currentStore?.id, 20)
}
/// URI
func testPermissionContextFlattensPermissionURIs() {
let permissionContext = PermissionContext()
permissionContext.replaceRolePermissions([
RolePermissionResponse(
role: RoleInfo(
id: 1,
name: "运营",
permission: [
PermissionItem(
id: 1,
name: "首页",
uri: "/home",
children: [
PermissionItem(id: 2, name: "订单", uri: "/orders")
]
)
]
),
scenic: []
)
])
XCTAssertTrue(permissionContext.canAccess("/home"))
XCTAssertTrue(permissionContext.canAccess("/orders"))
XCTAssertFalse(permissionContext.canAccess("/settings"))
}
}

View File

@ -0,0 +1,80 @@
//
// HomeCommonMenuStoreTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
///
final class HomeCommonMenuStoreTests: XCTestCase {
///
func testLoadCreatesDefaultCommonMenusWhenBaselineIsMissing() {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("registration_invitation"),
menu("location_report"),
menu("pm_manager")
]
let uris = store.load(menuItems: menus)
XCTAssertEqual(uris, ["registration_invitation", "location_report", "pm_manager"])
XCTAssertTrue(defaults.bool(forKey: HomeCommonMenuStore.defaultBaselineKey))
}
/// URI URI
func testLoadFiltersSavedUrisByCurrentPermissions() {
let defaults = makeIsolatedDefaults()
defaults.set(true, forKey: HomeCommonMenuStore.defaultBaselineKey)
defaults.set(["task_management", "unknown", "pm"], forKey: HomeCommonMenuStore.defaultStorageKey)
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("task_management_editor"),
menu("pm_manager")
]
let uris = store.load(menuItems: menus)
XCTAssertEqual(uris, ["task_management_editor", "pm_manager"])
}
/// URI
func testAddDeduplicatesAliases() {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("task_management_editor")
]
let uris = store.add("task_management", current: ["task_management_editor"], menuItems: menus)
XCTAssertEqual(uris, ["task_management_editor"])
}
/// URI
func testRemoveDeletesAliases() {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let uris = store.remove("pm", current: ["pm_manager", "location_report"])
XCTAssertEqual(uris, ["location_report"])
}
///
private func menu(_ uri: String) -> HomeMenuItem {
HomeMenuItem(title: HomeMenuRouter.title(for: uri), uri: uri, iconSrc: nil)
}
/// UserDefaults App
private func makeIsolatedDefaults() -> UserDefaults {
let suiteName = "suixinkan.home.tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return defaults
}
}

View File

@ -0,0 +1,152 @@
//
// HomeMenuRouterTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
/// URI
final class HomeMenuRouterTests: XCTestCase {
override func setUp() {
super.setUp()
HomeRouteDiagnostics.resetUnknownRoutes()
}
override func tearDown() {
HomeRouteDiagnostics.resetUnknownRoutes()
super.tearDown()
}
///
func testMigratedRoutesResolveToDestinations() {
XCTAssertEqual(HomeMenuRouter.resolve(uri: "space_settings", title: ""), .destination(.profileSpace))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenicselection", title: ""), .destination(.scenicSelection))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "store", title: ""), .destination(.moreFunctions))
}
///
func testKnownUnmigratedRoutesResolveToPlaceholders() {
XCTAssertEqual(
HomeMenuRouter.resolve(uri: "task_management", title: ""),
.destination(.modulePlaceholder(uri: "task_management", title: "任务管理"))
)
XCTAssertEqual(
HomeMenuRouter.resolve(uri: "payment_qr", title: ""),
.destination(.modulePlaceholder(uri: "payment_qr", title: "收款码"))
)
}
/// Tab HomeMenuRouter
func testTabRoutesStayCentralized() {
XCTAssertEqual(HomeMenuRouter.resolve(uri: "photographer_orders", title: ""), .tab(.orders))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "/scenic-order-manage", title: ""), .tab(.orders))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "verification_order", title: ""), .tab(.orders))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "photographer_stats", title: ""), .tab(.statistics))
}
///
func testFlightControlRoutesAreKnownUnsupportedScope() {
XCTAssertEqual(
HomeMenuRouter.resolve(uri: "fly", title: ""),
.unsupported(uri: "fly", title: "飞行管理", reason: "DJI/飞控模块已明确不纳入 iOS 迁移和后续开发范围。")
)
}
/// URI placeholder
func testUnknownRouteIsExplicitPlaceholder() {
XCTAssertEqual(
HomeMenuRouter.resolve(uri: "android_only_feature", title: "Android 专项"),
.placeholder(uri: "android_only_feature", title: "Android 专项")
)
}
/// canonicalURI 使 URI
func testCanonicalURIUsesAvailableAlias() {
let available: Set<String> = ["task_management_editor", "photographer_invite", "pm_manager", "payment_qr"]
XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "task_management", availableURIs: available), "task_management_editor")
XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "registration_invitation", availableURIs: available), "photographer_invite")
XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "pm", availableURIs: available), "pm_manager")
XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "payment_code", availableURIs: available), "payment_qr")
}
/// menuAliasKey
func testMenuAliasKeyGroupsDuplicateAndroidEntries() {
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "task_management_editor"), "task_management")
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "photographer_invite"), "registration_invitation")
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "pm_manager"), "pm")
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "payment_code"), "payment_collection")
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "deposit_order_shooting_info"), "deposit_order")
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "/scenic-order-manage"), "photographer_orders")
XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "android_only_module"), "android_only_module")
}
///
func testUnknownRouteDiagnosticsDeduplicatesAndUpdatesTitle() {
HomeRouteDiagnostics.recordUnknown(uri: "android_feature", title: "旧标题")
HomeRouteDiagnostics.recordUnknown(uri: "android_feature", title: "新标题")
HomeRouteDiagnostics.recordUnknown(uri: "another_feature", title: "")
let records = HomeRouteDiagnostics.unknownRoutes()
XCTAssertEqual(records.count, 2)
XCTAssertEqual(records.first { $0.uri == "android_feature" }?.title, "新标题")
XCTAssertEqual(records.first { $0.uri == "android_feature" }?.count, 2)
XCTAssertEqual(records.first { $0.uri == "another_feature" }?.count, 1)
}
/// Markdown
func testUnknownRouteDiagnosticsReportIsMarkdownAndSortedByCount() {
HomeRouteDiagnostics.recordUnknown(uri: "rare_feature", title: "低频入口")
HomeRouteDiagnostics.recordUnknown(uri: "hot_feature", title: "高频入口")
HomeRouteDiagnostics.recordUnknown(uri: "hot_feature", title: "高频入口")
let report = HomeRouteDiagnostics.unknownRouteReport(generatedAt: Date(timeIntervalSince1970: 0))
XCTAssertTrue(report.contains("# Home Route Diagnostics"))
XCTAssertTrue(report.contains("| hot_feature | 高频入口 | 2 |"))
XCTAssertLessThan(
report.range(of: "hot_feature")!.lowerBound,
report.range(of: "rare_feature")!.lowerBound
)
}
///
func testPermissionRouteAuditSeparatesRoutableUnsupportedAndUnknownEntries() {
let permissions = [
rolePermission(
roleId: 1,
permissions: [
permission(id: 1, name: "任务管理", uri: "task_management", children: [
permission(id: 2, name: "发布任务", uri: "task_create")
]),
permission(id: 3, name: "飞控", uri: "pilot_controller"),
permission(id: 4, name: "Android 专项入口", uri: "android_only_module"),
permission(id: 5, name: "重复任务", uri: "task_management")
]
)
]
let audit = HomePermissionRouteAuditor.audit(permissions: permissions, currentRoleId: 1)
XCTAssertEqual(audit.routable.map(\.uri), ["task_management", "task_create"])
XCTAssertEqual(audit.unsupported.map(\.uri), ["pilot_controller"])
XCTAssertEqual(audit.unknown.map(\.uri), ["android_only_module"])
XCTAssertTrue(audit.hasUnknownRoutes)
}
///
private func rolePermission(roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse {
RolePermissionResponse(
role: RoleInfo(id: roleId, name: "角色\(roleId)", permission: permissions),
scenic: []
)
}
///
private func permission(id: Int, name: String, uri: String, children: [PermissionItem] = []) -> PermissionItem {
PermissionItem(id: id, name: name, uri: uri, children: children)
}
}

View File

@ -0,0 +1,111 @@
//
// HomeViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class HomeViewModelTests: XCTestCase {
/// 退
func testBuildMenusClearsStaleMenusWhenCurrentRoleIsMissing() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
permission(id: 1, name: "消息中心", uri: "message_center")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["message_center"])
viewModel.buildMenus(from: permissions, currentRoleId: 99)
XCTAssertTrue(viewModel.menuItems.isEmpty)
}
/// 退
func testBuildMenusFallsBackToFirstRoleOnlyWhenCurrentRoleIsNil() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
permission(id: 1, name: "任务管理", uri: "task_management")
]),
rolePermission(roleId: 2, permissions: [
permission(id: 2, name: "消息中心", uri: "message_center")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: nil)
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["task_management"])
}
///
func testBuildMenusFlattensPermissionTree() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
permission(id: 1, name: "任务管理", uri: "task_management", children: [
permission(id: 2, name: "消息中心", uri: "message_center")
])
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["task_management", "message_center"])
}
/// URI
func testBuildMenusDeduplicatesKnownAndroidAliases() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
permission(id: 1, name: "项目管理", uri: "pm_manager"),
permission(id: 2, name: "项目管理旧入口", uri: "pm"),
permission(id: 3, name: "收款码", uri: "payment_code"),
permission(id: 4, name: "收款码旧入口", uri: "payment_qr"),
permission(id: 5, name: "押金订单详情", uri: "deposit_order_detail"),
permission(id: 6, name: "押金订单", uri: "deposit_order")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["deposit_order_detail", "pm_manager", "payment_code"])
}
/// preferred order
func testBuildMenusSortsByPreferredOrder() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
permission(id: 1, name: "消息中心", uri: "message_center"),
permission(id: 2, name: "空间设置", uri: "space_settings"),
permission(id: 3, name: "钱包", uri: "wallet")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["space_settings", "wallet", "message_center"])
}
///
private func rolePermission(roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse {
RolePermissionResponse(
role: RoleInfo(id: roleId, name: "角色\(roleId)", permission: permissions),
scenic: []
)
}
///
private func permission(id: Int, name: String, uri: String, children: [PermissionItem] = []) -> PermissionItem {
PermissionItem(id: id, name: name, uri: uri, children: children)
}
}

View File

@ -0,0 +1,52 @@
//
// LoginViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/20.
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class LoginViewModelTests: XCTestCase {
/// +86 11
func testNormalizeUsernameCountryCodeRemovesChinaPrefix() {
let viewModel = LoginViewModel()
viewModel.username = "+86 186 5185 7230"
viewModel.normalizeUsernameCountryCodeIfNeeded()
XCTAssertEqual(viewModel.username, "18651857230")
XCTAssertEqual(viewModel.normalizedUsername, "18651857230")
XCTAssertTrue(viewModel.isValidPhone)
}
///
func testValidateForLoginRequiresPrivacyAgreement() {
let viewModel = LoginViewModel()
viewModel.username = "18651857230"
viewModel.password = "secret123"
viewModel.privacyChecked = false
XCTAssertEqual(viewModel.validateForLogin(), .privacyUnchecked)
}
///
func testApplyPreferencesRestoresUsernameAndPrivacyOnly() {
let viewModel = LoginViewModel()
viewModel.password = "manual-password"
viewModel.applyPreferences(
LoginPreferences(
lastUsername: "18651857230",
privacyAgreementAccepted: true
)
)
XCTAssertEqual(viewModel.username, "18651857230")
XCTAssertTrue(viewModel.privacyChecked)
XCTAssertEqual(viewModel.password, "manual-password")
}
}

View File

@ -0,0 +1,18 @@
//
// NavigationRouterTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
/// AppRoute
final class NavigationRouterTests: XCTestCase {
/// NavigationStack push TabBar
func testPushedRoutesHideTabBarByDefault() {
XCTAssertTrue(AppRoute.placeholder(title: "详情").hidesTabBarWhenPushed)
XCTAssertTrue(AppRoute.home(.moreFunctions).hidesTabBarWhenPushed)
}
}

View File

@ -0,0 +1,76 @@
//
// ScenicSpotContextTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class ScenicSpotContextTests: XCTestCase {
/// ID
func testReloadRequestsSpotsForScenicId() async {
let context = ScenicSpotContext()
let api = MockScenicSpotAPI()
api.spotResponse = ListPayload(total: 1, list: [ScenicSpotItem(id: 9, name: "观景台")])
await context.reload(scenicId: 88, api: api)
XCTAssertEqual(api.requestedScenicIds, [88])
XCTAssertEqual(context.scenicId, 88)
XCTAssertEqual(context.spots.map(\.id), [9])
XCTAssertEqual(context.loadState, .loaded)
}
///
func testReloadFailureOnlyAffectsScenicSpotContext() async {
let context = ScenicSpotContext()
let api = MockScenicSpotAPI()
api.error = APIError.networkFailed("景点接口失败")
await context.reload(scenicId: 88, api: api)
XCTAssertEqual(api.requestedScenicIds, [88])
XCTAssertTrue(context.spots.isEmpty)
if case .failed = context.loadState {
XCTAssertTrue(true)
} else {
XCTFail("景点接口失败时应进入 failed 状态")
}
}
}
@MainActor
/// ID
private final class MockScenicSpotAPI: AccountContextServing {
var requestedScenicIds: [Int] = []
var spotResponse = ListPayload(total: 0, list: [ScenicSpotItem]())
var error: Error?
/// 使
func rolePermissions() async throws -> [RolePermissionResponse] {
[]
}
/// 使
func scenicListAll() async throws -> ScenicListAllResponse {
ScenicListAllResponse(total: 0, list: [])
}
/// 使
func storeAll() async throws -> ListPayload<StoreItem> {
ListPayload(total: 0, list: [])
}
/// ID
func scenicSpotListAll(scenicId: Int) async throws -> ListPayload<ScenicSpotItem> {
requestedScenicIds.append(scenicId)
if let error {
throw error
}
return spotResponse
}
}

View File

@ -0,0 +1,60 @@
//
// StorageTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/20.
//
import XCTest
@testable import suixinkan
/// Store UserDefaults
final class StorageTests: XCTestCase {
/// AppPreferencesStore
func testAppPreferencesStorePersistsLoginPreferences() {
let defaults = makeIsolatedDefaults()
let store = AppPreferencesStore(defaults: defaults)
store.saveLastLoginUsername(" 18651857230 ")
store.savePrivacyAgreementAccepted(true)
XCTAssertEqual(store.loadLastLoginUsername(), "18651857230")
XCTAssertTrue(store.loadPrivacyAgreementAccepted())
}
///
func testAccountSnapshotStorePersistsAccountContextSnapshot() {
let defaults = makeIsolatedDefaults()
let store = AccountSnapshotStore(defaults: defaults)
let snapshot = AccountSnapshot(
profile: AccountProfile(
userId: "101",
displayName: "测试账号",
phone: "18651857230",
avatarURL: "https://cdn.example.com/avatar.jpg"
),
accountType: "store_user",
businessUserId: 101,
scenicScopes: [
BusinessScope(id: 88, name: "东门景区", kind: .scenic)
],
storeScopes: [
BusinessScope(id: 66, name: "东门门店", kind: .store)
],
currentScenicId: 88,
currentStoreId: 66
)
store.save(snapshot)
XCTAssertEqual(store.load(), snapshot)
}
/// UserDefaults App
private func makeIsolatedDefaults() -> UserDefaults {
let suiteName = "suixinkan.tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return defaults
}
}