// // AppFeatureServicesTests.swift // suixinkanTests // // Created by Codex on 2026/6/29. // import XCTest @testable import suixinkan_ios @MainActor /// 模块依赖门面测试,覆盖上下文派生和独立单例运行时边界。 final class AppFeatureServicesTests: XCTestCase { /// 订单门面应从全局账号和权限上下文派生当前业务参数。 func testOrdersFeatureServicesReflectsCurrentContext() { let services = makeServices() applyContext(to: services) let feature = services.ordersFeatureServices XCTAssertIdentical(feature.api as AnyObject, services.ordersAPI as AnyObject) XCTAssertEqual(feature.currentScenicId, 7) XCTAssertEqual(feature.currentStoreId, 9) XCTAssertEqual(feature.currentRoleId, 3) XCTAssertIdentical(feature.navigator as AnyObject, services.appNavigator as AnyObject) XCTAssertIdentical(feature.toastCenter as AnyObject, services.toastCenter as AnyObject) XCTAssertIdentical(feature.globalLoading as AnyObject, services.globalLoading as AnyObject) } /// 资产门面应提供共享传输仓库和上传依赖。 func testAssetsFeatureServicesUsesSharedTransferStore() { let services = makeServices() applyContext(to: services) let feature = services.assetsFeatureServices XCTAssertIdentical(feature.api as AnyObject, services.assetsAPI as AnyObject) XCTAssertIdentical(feature.uploader as AnyObject, services.ossUploadService as AnyObject) XCTAssertIdentical(feature.transferStore as AnyObject, CloudTransferStore.shared as AnyObject) XCTAssertEqual(feature.currentScenicId, 7) } /// 排队门面应暴露 App 级共享运行时和当前用户景区上下文。 func testQueueFeatureServicesUsesSharedRuntimeAndContext() { let services = makeServices() applyContext(to: services) let feature = services.queueFeatureServices XCTAssertIdentical(feature.api as AnyObject, services.scenicQueueAPI as AnyObject) XCTAssertIdentical(feature.runtime as AnyObject, ScenicQueueRuntime.shared as AnyObject) XCTAssertIdentical(feature.scenicSpotContext as AnyObject, services.scenicSpotContext as AnyObject) XCTAssertEqual(feature.currentScenicId, 7) XCTAssertEqual(feature.userId, "42") } /// 个人中心门面执行退出登录时,应清理登录态并停止共享排队运行时。 func testProfileFeatureLogoutStopsQueueRuntime() { let services = makeServices() applyContext(to: services) services.appSession.markLoggedIn(token: "token") ScenicQueueSettingsStore.saveSelectedSpot(id: 11, name: "一号点", userId: "42", scenicId: 7) let runtime = ScenicQueueRuntime.shared defer { runtime.stop() clearQueueSelection(userId: "42", scenicId: 7) } runtime.update(api: AppFeatureScenicQueueMock(), userId: "42", scenicId: 7, scenePhase: .active) XCTAssertTrue(runtime.isMonitoring) services.profileFeatureServices.logout() XCTAssertFalse(runtime.isMonitoring) XCTAssertFalse(services.appSession.isLoggedIn) XCTAssertNil(services.accountContext.profile) } /// 创建带隔离网络会话的测试容器。 private func makeServices() -> AppServices { AppServices(apiClient: APIClient(session: AppFeatureRecordingURLSession())) } /// 写入测试所需的账号、景区、门店和角色上下文。 private func applyContext(to services: AppServices) { services.accountContext.applyLogin( profile: AccountProfile( userId: "42", displayName: "测试用户", phone: "18600000000", avatarURL: nil ) ) services.accountContext.replaceScopes( scenic: [BusinessScope(id: 7, name: "测试景区", kind: .scenic)], stores: [BusinessScope(id: 9, name: "测试门店", kind: .store, parentScenicId: 7)] ) services.permissionContext.currentRole = RoleInfo(id: 3, name: "店长") } /// 清理排队运行时测试写入的本地选中点。 private func clearQueueSelection(userId: String, scenicId: Int) { let defaults = UserDefaults.standard defaults.removeObject(forKey: ScenicQueueLocalSettings.selectedSpotIdKey) defaults.removeObject(forKey: ScenicQueueLocalSettings.selectedSpotNameKey) defaults.removeObject(forKey: ScenicQueueSettingsStore.scopedKey( base: ScenicQueueLocalSettings.selectedSpotIdKey, userId: userId, scenicId: scenicId )) defaults.removeObject(forKey: ScenicQueueSettingsStore.scopedKey( base: ScenicQueueLocalSettings.selectedSpotNameKey, userId: userId, scenicId: scenicId )) } } /// 排队运行时测试替身,避免触发真实网络请求。 @MainActor private final class AppFeatureScenicQueueMock: ScenicQueueServing { func scenicQueueStats(scenicId: Int, scenicSpotId: Int) async throws -> ScenicQueueStatsData { ScenicQueueStatsData() } func scenicQueueHome( scenicId: Int, scenicSpotId: Int, type: Int, page: Int, pageSize: Int ) async throws -> ScenicQueueHomeData { ScenicQueueHomeData() } func socketToken() async throws -> SocketTokenResponse { SocketTokenResponse(socketToken: "token") } func scenicQueueCall(id: Int64) async throws -> ScenicQueueCallData { ScenicQueueCallData(id: id) } func scenicQueuePass(id: Int64) async throws -> ScenicQueuePassData { ScenicQueuePassData(id: id) } func scenicQueueFinish(id: Int64) async throws -> ScenicQueueFinishData { ScenicQueueFinishData(id: id) } func scenicQueueRequeueInsertBefore(recordId: Int64, operatorId: Int) async throws {} func scenicQueueUserMark(_ request: ScenicQueueUserMarkRequest) async throws {} func scenicQueueSetting(scenicId: Int, scenicSpotId: Int?) async throws -> ScenicQueueSettingData { ScenicQueueSettingData() } func scenicQueueSaveSetting(_ request: ScenicQueueSaveSettingRequest) async throws {} func scenicQueueShootQueueQRCode(scenicId: Int, scenicSpotId: Int) async throws -> ScenicQueueShootQueueQRCodeData { ScenicQueueShootQueueQRCodeData(qrcodeUrl: "") } func scenicQueueSettingChangeLog( scenicId: Int, scenicSpotId: Int?, page: Int, pageSize: Int ) async throws -> ScenicQueueSettingChangeLogData { ScenicQueueSettingChangeLogData() } } /// 不发起真实网络请求的 URLSession 测试替身。 private final class AppFeatureRecordingURLSession: URLSessionProtocol { /// 返回空响应数据。 func data(for request: URLRequest) async throws -> (Data, URLResponse) { let response = HTTPURLResponse( url: request.url ?? URL(string: "https://example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil )! return (Data(), response) } }