// // AppServicesTests.swift // suixinkanTests // // Created by Codex on 2026/6/26. // import XCTest @testable import suixinkan_ios @MainActor /// `AppServices` 依赖容器测试,覆盖分组结构与测试专用初始化。 final class AppServicesTests: XCTestCase { /// 测试专用初始化应创建独立实例,且各 API 共享同一 `APIClient`。 func testTestingInitializerCreatesGroupedDependenciesWithSharedClient() async throws { let session = AppServicesRecordingURLSession(responses: [ try TestFixture.data(named: "writeoff_list_success"), try TestFixture.data(named: "user_info_success") ]) let apiClient = APIClient(session: session) let services = AppServices(apiClient: apiClient) XCTAssertNotIdentical(services.network.apiClient as AnyObject, AppServices.shared.network.apiClient as AnyObject) _ = try await services.ordersAPI.writeOffList(scenicId: 1, storeId: nil, page: 1, pageSize: 1) _ = try await services.profileAPI.userInfo() XCTAssertEqual(session.requests.count, 2) XCTAssertNotNil(services.session.appSession) XCTAssertNotNil(services.context.accountContext) XCTAssertNotNil(services.ui.toastCenter) XCTAssertNotNil(services.runtime.scenicQueueRuntime) } /// 扁平属性应转发到对应 bundle,保持现有调用方兼容。 func testFlatAccessorsForwardToBundles() { 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.toastCenter as AnyObject, services.ui.toastCenter as AnyObject) XCTAssertIdentical(services.scenicQueueRuntime as AnyObject, services.runtime.scenicQueueRuntime as AnyObject) } /// `APIClient` 应通过 `AppSession.token` 提供鉴权 token。 func testAuthTokenProviderUsesAppSessionToken() async throws { let session = AppServicesRecordingURLSession(data: try TestFixture.data(named: "user_info_success")) let apiClient = APIClient(session: session) let services = AppServices(apiClient: apiClient) services.appSession.markLoggedIn(token: "test-token") _ = try await services.profileAPI.userInfo() let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.value(forHTTPHeaderField: "token"), "test-token") } /// 便捷属性应基于 `AccountContext` 派生当前用户与景区信息。 func testConveniencePropertiesReflectAccountContext() { let services = AppServices(apiClient: APIClient(session: AppServicesRecordingURLSession())) services.accountContext.applyLogin( profile: AccountProfile( userId: "42", displayName: "测试用户", phone: "18600000000", avatarURL: nil ) ) services.accountContext.currentScenic = BusinessScope(id: 7, name: "测试景区", kind: .scenic) XCTAssertEqual(services.userId, "42") XCTAssertEqual(services.staffId, 42) XCTAssertEqual(services.currentScenicId, 7) } } /// 记录请求的 URLSession 测试替身。 private final class AppServicesRecordingURLSession: URLSessionProtocol { private(set) var requests: [URLRequest] = [] private let responses: [Data] private var responseIndex = 0 /// 初始化实例。 init(data: Data = Data()) { responses = [data] } /// 按顺序返回多段响应数据。 init(responses: [Data]) { self.responses = responses } /// 记录请求并返回固定响应。 func data(for request: URLRequest) async throws -> (Data, URLResponse) { requests.append(request) let data = responses[min(responseIndex, responses.count - 1)] responseIndex += 1 let response = HTTPURLResponse( url: request.url ?? URL(string: "https://example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil )! return (data, response) } }