// // ProfileSecondaryPagesTests.swift // suixinkanTests // // Created by Codex on 2026/6/22. // import XCTest @testable import suixinkan @MainActor /// 个人中心二级页面测试,覆盖设置、账号切换和实名认证基础逻辑。 final class ProfileSecondaryPagesTests: XCTestCase { /// 测试设置页版本号展示规则。 func testSettingsVersionTextUsesBuildNumberWhenNeeded() { XCTAssertEqual( SettingsDisplayPolicy.versionText(infoDictionary: [ "CFBundleShortVersionString": "1.2", "CFBundleVersion": "45" ]), "1.2.45" ) XCTAssertEqual( SettingsDisplayPolicy.versionText(infoDictionary: [ "CFBundleShortVersionString": "1.2.3", "CFBundleVersion": "45" ]), "1.2.3" ) XCTAssertEqual(SettingsDisplayPolicy.versionText(infoDictionary: nil), "1.0.0") } /// 测试协议页面映射到预期 H5 路径。 func testAgreementPagesUseExpectedPaths() { let pages: [(AgreementPage, String, String)] = [ (.about, "关于我们", "/h5/app/about-us"), (.userAgreement, "用户协议", "/h5/app/user-agreement"), (.privacyPolicy, "隐私政策", "/h5/app/privacy-policy"), (.walletUserNotice, "钱包用户须知", "/h5/app/wallet-user-notice"), (.walletPrivacy, "钱包隐私政策", "/h5/app/wallet-privacy") ] for (page, title, path) in pages { XCTAssertEqual(page.title, title) XCTAssertEqual(page.id, title) XCTAssertEqual(page.url.path, path) } } /// 测试账号切换列表接口使用旧工程一致的 v9 accounts 路径。 func testSwitchableAccountsUsesExpectedEndpoint() async throws { let session = ProfileSecondaryURLSession(responses: [ try TestFixture.data(named: "v9_login_multi_success") ]) let api = ProfileAPI(client: APIClient(session: session)) let response = try await api.switchableAccounts() XCTAssertEqual(response.accounts.count, 2) let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url?.path, "/api/app/v9/accounts") } /// 测试账号切换 ViewModel 能加载账号并默认选中第一项。 func testAccountSwitchViewModelLoadsAccounts() async throws { let session = ProfileSecondaryURLSession(responses: [ try TestFixture.data(named: "v9_login_multi_success") ]) let api = ProfileAPI(client: APIClient(session: session)) let viewModel = AccountSwitchViewModel() try await viewModel.load(api: api) XCTAssertEqual(viewModel.accounts.map(\.businessUserId), [101, 201]) XCTAssertEqual(viewModel.selectedAccount?.businessUserId, 101) XCTAssertFalse(viewModel.loading) } /// 测试账号切换提交使用 set-user 接口和账号对应请求体。 func testAccountSwitchViewModelSubmitsSetUser() async throws { let session = ProfileSecondaryURLSession(responses: [ try TestFixture.data(named: "v9_set_store_user_success") ]) let api = AuthAPI(client: APIClient(session: session)) let account = AccountSwitchAccount( accountType: V9StoreUser.accountTypeValue, businessUserId: 201, title: "示例门店", subtitle: "示例景区", phone: "13800138000", avatar: "", scenicName: "示例景区", storeId: 20, storeName: "示例门店", scenicId: 10, isCurrent: false ) let viewModel = AccountSwitchViewModel() let response = try await viewModel.switchAccount(account, api: api) XCTAssertFalse(response.token.isEmpty) let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.httpMethod, "POST") XCTAssertEqual(request.url?.path, "/api/app/v9/set-user") let json = try XCTUnwrap(JSONSerialization.jsonObject(with: XCTUnwrap(request.httpBody)) as? [String: Any]) XCTAssertEqual(json["store_user_id"] as? Int, 201) XCTAssertNil(json["ss_user_id"] as? Int) } /// 测试实名认证信息解码兼容字符串数字字段。 func testRealNameInfoDecodesLossyFields() async throws { let session = ProfileSecondaryURLSession(responses: [ try TestFixture.data(named: "real_name_info_success") ]) let api = ProfileAPI(client: APIClient(session: session)) let response = try await api.realNameInfo() let info = try XCTUnwrap(response.realNameInfo) XCTAssertEqual(info.realName, "测试摄影师") XCTAssertEqual(info.idCardNo, "320100********0012") XCTAssertTrue(info.verified) XCTAssertFalse(info.isLongValid) XCTAssertEqual(info.auditStatus, 2) XCTAssertEqual(info.auditorId, 7) XCTAssertEqual(info.auditor?.name, "审核员") XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/real-name/info") } /// 测试实名认证短信和提交接口路径及请求体。 func testRealNameSmsAndSubmitUseExpectedEndpoints() async throws { let session = ProfileSecondaryURLSession(responses: [ try TestFixture.data(named: "empty_success"), try TestFixture.data(named: "empty_success") ]) let api = ProfileAPI(client: APIClient(session: session)) try await api.realNameSmsVerifyCode() try await api.realNameSubmit( RealNameAuthRequest( realName: "测试摄影师", idCardNo: "11010519491231002X", smsVerifyCode: "123456", startDate: "2026-01-01", endDate: "2046-01-01", isLongValid: 0, frontUrl: "https://cdn.example.com/front.jpg", backUrl: "https://cdn.example.com/back.jpg" ) ) XCTAssertEqual(session.requests.map { $0.url?.path }, [ "/api/yf-handset-app/photog/real-name/sms-verify-code", "/api/yf-handset-app/photog/real-name/submit" ]) let json = try XCTUnwrap(JSONSerialization.jsonObject(with: XCTUnwrap(session.requests.last?.httpBody)) as? [String: Any]) XCTAssertEqual(json["real_name"] as? String, "测试摄影师") XCTAssertEqual(json["id_card_no"] as? String, "11010519491231002X") XCTAssertEqual(json["sms_verify_code"] as? String, "123456") XCTAssertEqual(json["is_long_valid"] as? Int, 0) } /// 测试身份证号校验和格式归一化。 func testRealNameIDCardValidation() { XCTAssertTrue(RealNameAuthViewModel.isValidMainlandIDCardNumber("11010519491231002x")) XCTAssertEqual(RealNameAuthViewModel.normalizedIDCardNumber(" 11010519491231002x "), "11010519491231002X") XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("110105194912310021")) XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("123")) } /// 测试首页系统设置 URI 已接入真实设置页。 func testHomeSystemSettingsRouteResolvesToSettingsPage() { XCTAssertEqual( HomeMenuRouter.resolve(uri: "system_settings", title: "设置中心"), .destination(.settings) ) } } /// 个人中心测试用 URLSession 替身,按顺序返回预设响应。 private final class ProfileSecondaryURLSession: URLSessionProtocol { private var responses: [Data] private(set) var requests: [URLRequest] = [] /// 初始化顺序响应。 init(responses: [Data]) { self.responses = responses } /// 记录请求并返回下一份响应。 func data(for request: URLRequest) async throws -> (Data, URLResponse) { requests.append(request) let data = responses.isEmpty ? Data() : responses.removeFirst() return ( data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! ) } }