Refactor AppServices into assembly, feature facades, and lifecycle coordinator.
Extract dependency wiring and session lifecycle from the container and RootViewController, split NetworkBundle by domain, and add module-level feature services while keeping flat accessors for compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
169
suixinkan_iosTests/AppFeatureServicesTests.swift
Normal file
169
suixinkan_iosTests/AppFeatureServicesTests.swift
Normal file
@ -0,0 +1,169 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,7 @@ final class AppServicesTests: XCTestCase {
|
||||
XCTAssertNotNil(services.context.accountContext)
|
||||
XCTAssertNotNil(services.ui.toastCenter)
|
||||
XCTAssertNotNil(services.runtime.scenicQueueRuntime)
|
||||
XCTAssertIdentical(services.network.apiClient as AnyObject, services.apiClient as AnyObject)
|
||||
}
|
||||
|
||||
/// 扁平属性应转发到对应 bundle,保持现有调用方兼容。
|
||||
@ -35,7 +36,10 @@ final class AppServicesTests: XCTestCase {
|
||||
let services = AppServices(apiClient: APIClient(session: AppServicesRecordingURLSession()))
|
||||
|
||||
XCTAssertIdentical(services.appSession as AnyObject, services.session.appSession as AnyObject)
|
||||
XCTAssertIdentical(services.ordersAPI as AnyObject, services.network.ordersAPI as AnyObject)
|
||||
XCTAssertIdentical(services.authAPI as AnyObject, services.network.account.authAPI as AnyObject)
|
||||
XCTAssertIdentical(services.ordersAPI as AnyObject, services.network.commerce.ordersAPI as AnyObject)
|
||||
XCTAssertIdentical(services.scenicQueueAPI as AnyObject, services.network.operation.scenicQueueAPI as AnyObject)
|
||||
XCTAssertIdentical(services.assetsAPI as AnyObject, services.network.content.assetsAPI as AnyObject)
|
||||
XCTAssertIdentical(services.toastCenter as AnyObject, services.ui.toastCenter as AnyObject)
|
||||
XCTAssertIdentical(services.scenicQueueRuntime as AnyObject, services.runtime.scenicQueueRuntime as AnyObject)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user