// // InviteTests.swift // suixinkanTests // // Created by Codex on 2026/6/24. // import XCTest @testable import suixinkan_ios @MainActor /// 邀请 API 和 ViewModel 测试。 final class InviteTests: XCTestCase { /// 测试邀请 API path 和分页参数。 func testInviteAPIUsesExpectedRequests() async throws { let session = InviteRecordingURLSession(responses: [Self.inviteInfoResponse, Self.inviteUsersResponse]) let api = InviteAPI(client: APIClient(session: session)) let info = try await api.inviteInfo() let users = try await api.inviteUserList(page: 0, pageSize: 0) XCTAssertEqual(info.inviteCode, "ABC123") XCTAssertEqual(users.first?.id, 1) XCTAssertEqual(session.requests.map { $0.url?.path }, [ "/api/yf-handset-app/photog/invite-info", "/api/yf-handset-app/photog/invite/user-list" ]) XCTAssertEqual(inviteQueryItems(from: session.requests[1])["page"], "1") XCTAssertEqual(inviteQueryItems(from: session.requests[1])["page_size"], "1") } /// 测试邀请页加载信息并生成二维码。 func testInviteViewModelLoadsInfoAndGeneratesQRCode() async { let api = FakeInviteService() let viewModel = PhotographerInviteViewModel() await viewModel.reload(api: api) XCTAssertEqual(viewModel.inviteCode, "ABC123") XCTAssertEqual(viewModel.rules, ["规则1"]) XCTAssertNotNil(viewModel.qrImage) } /// 测试邀请记录加载邀请用户和钱包汇总。 func testInviteRecordLoadsInviteRowsAndSummary() async { let inviteAPI = FakeInviteService() let walletAPI = FakeWalletService() let viewModel = InviteRecordViewModel() await viewModel.reload(inviteAPI: inviteAPI, walletAPI: walletAPI, refresh: true) XCTAssertEqual(viewModel.displayRows.first?.title, "摄影师") XCTAssertEqual(viewModel.totalRewardText, "¥ 100.00") XCTAssertEqual(walletAPI.summaryTypes, [2]) } /// 测试奖励分段调用钱包收益明细。 func testInviteRecordRewardTabLoadsWalletDetails() async { let inviteAPI = FakeInviteService() let walletAPI = FakeWalletService() let viewModel = InviteRecordViewModel() await viewModel.selectTab(.reward, inviteAPI: inviteAPI, walletAPI: walletAPI) XCTAssertEqual(walletAPI.earningPages, [1]) XCTAssertNil(viewModel.errorMessage, viewModel.errorMessage ?? "") XCTAssertEqual(viewModel.displayRows.first?.amount, "8.00") } private static let inviteInfoResponse = Data(#"{"code":100000,"message":"ok","data":{"enable_invite":"1","invite_code":"ABC123","invite_url":"https://invite.example.com","description":["规则1"]}}"#.utf8) private static let inviteUsersResponse = Data(#"{"code":100000,"message":"ok","data":[{"id":"1","real_name":"摄影师","phone":"13800000000","avatar":"","created_at":"2026-06-24","invite_level":"1"}]}"#.utf8) } /// 邀请 API 测试 URLSession。 private final class InviteRecordingURLSession: URLSessionProtocol { private var responses: [Data] private(set) var requests: [URLRequest] = [] /// 初始化测试 Session。 init(responses: [Data]) { self.responses = responses } /// 记录请求并返回响应。 func data(for request: URLRequest) async throws -> (Data, URLResponse) { requests.append(request) let data = responses.removeFirst() return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!) } } /// 邀请服务测试替身。 @MainActor private final class FakeInviteService: InviteServing { func inviteInfo() async throws -> InviteInfoResponse { try JSONDecoder().decode(InviteInfoResponse.self, from: Data(#"{"enable_invite":"1","invite_code":"ABC123","invite_url":"https://invite.example.com","description":["规则1"]}"#.utf8)) } func inviteUserList(page: Int, pageSize: Int) async throws -> [InviteUserItem] { [try JSONDecoder().decode(InviteUserItem.self, from: Data(#"{"id":"1","real_name":"摄影师","phone":"13800000000","avatar":"","created_at":"2026-06-24","invite_level":"1"}"#.utf8))] } } /// 钱包服务测试替身。 @MainActor private final class FakeWalletService: WalletServing { var summaryTypes: [Int] = [] var earningPages: [Int] = [] func walletSummary(type: Int) async throws -> WalletSummaryResponse { summaryTypes.append(type) return WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "20.00", amountWithdrawable: "50.00") } func walletEarningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> WalletEarningDetailResponse { earningPages.append(page) let item = WalletEarningDetailItem( id: 1, amount: "8.00", type: 2, typeLabel: "邀请奖励", orderNumberSuffix: "0001", createdAt: "2026-06-24" ) return WalletEarningDetailResponse( totalAmount: "8.00", total: 1, list: [WalletEarningDetailGroup(date: "2026-06-24", dayAmount: "8.00", items: [item])] ) } func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse { WalletWithdrawListResponse() } func withdrawInfo() async throws -> WithdrawInfoResponse { try JSONDecoder().decode(WithdrawInfoResponse.self, from: Data("{}".utf8)) } func withdrawSendSms() async throws {} func withdrawApply(amount: String, smsCode: String) async throws {} func bankCardInfo() async throws -> BankCardInfoResponse { try JSONDecoder().decode(BankCardInfoResponse.self, from: Data("{}".utf8)) } func bankList() async throws -> BankListResponse { BankListResponse(banks: []) } func areas() async throws -> [AreaNode] { [] } func bankCardVerifyCode() async throws {} func updateBankInfo(_ request: UpdateBankInfoRequest) async throws {} func pointOverview(staffId: Int) async throws -> PointOverviewResponse { try JSONDecoder().decode(PointOverviewResponse.self, from: Data("{}".utf8)) } func pointWithdrawApply(points: Int, remark: String) async throws {} func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse { PointWithdrawListResponse(total: 0, list: []) } } /// 从请求中提取 query 字典。 private func inviteQueryItems(from request: URLRequest) -> [String: String] { guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return [:] } return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in item.value.map { (item.name, $0) } }) }