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>
170 lines
7.0 KiB
Swift
170 lines
7.0 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|