// // WithdrawalAuditViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/25. // import XCTest @testable import suixinkan @MainActor /// 提现审核测试,覆盖分页、筛选和失败清理。 final class WithdrawalAuditViewModelTests: XCTestCase { /// 测试首次加载、筛选统计和分页加载。 func testLoadFilterAndLoadMore() async throws { let api = WithdrawalAuditWalletMock() api.withdrawResponses = [ WalletWithdrawListResponse(total: 3, item: [ try record(id: 1, status: "审核中"), try record(id: 2, status: "已到账") ]), WalletWithdrawListResponse(total: 3, item: [ try record(id: 3, status: "待打款") ]) ] let viewModel = WithdrawalAuditViewModel() await viewModel.reload(api: api) XCTAssertEqual(api.withdrawPages, [1]) XCTAssertEqual(viewModel.totalCount, 3) XCTAssertEqual(viewModel.processingCount, 1) XCTAssertEqual(viewModel.completedCount, 1) XCTAssertTrue(viewModel.hasMore) viewModel.selectFilter(.processing) XCTAssertEqual(viewModel.filteredRecords.map(\.id), [1]) await viewModel.loadMore(api: api) XCTAssertEqual(api.withdrawPages, [1, 2]) XCTAssertEqual(viewModel.records.map(\.id), [1, 2, 3]) XCTAssertEqual(viewModel.processingCount, 2) XCTAssertFalse(viewModel.hasMore) } /// 测试刷新失败会清空旧记录和分页状态。 func testReloadFailureClearsStaleRecordsAndPaging() async throws { let api = WithdrawalAuditWalletMock() api.withdrawResponses = [ WalletWithdrawListResponse(total: 2, item: [try record(id: 1, status: "审核中")]) ] let viewModel = WithdrawalAuditViewModel() await viewModel.reload(api: api) XCTAssertEqual(viewModel.records.map(\.id), [1]) XCTAssertTrue(viewModel.hasMore) api.withdrawError = TestError.sample await viewModel.reload(api: api) XCTAssertTrue(viewModel.records.isEmpty) XCTAssertFalse(viewModel.hasMore) XCTAssertTrue(viewModel.loadFailed) XCTAssertEqual(viewModel.loadFailureReason, TestError.sample.localizedDescription) } /// 测试空列表和尾页边界不会继续加载。 func testEmptyListAndLastPageDoNotLoadMore() async { let api = WithdrawalAuditWalletMock() api.withdrawResponses = [WalletWithdrawListResponse(total: 0, item: [])] let viewModel = WithdrawalAuditViewModel() await viewModel.reload(api: api) await viewModel.loadMore(api: api) XCTAssertTrue(viewModel.records.isEmpty) XCTAssertEqual(api.withdrawPages, [1]) XCTAssertFalse(viewModel.hasMore) } private func record(id: Int64, amount: String = "10", status: String) throws -> WalletWithdrawRecord { let data = """ {"id":\(id),"amount":"\(amount)","created_at":"2026-06-25 10:00","status_label":"\(status)","expected_at":"2026-06-26","audit_time":"2026-06-25 11:00","completed_at":""} """.data(using: .utf8)! return try JSONDecoder().decode(WalletWithdrawRecord.self, from: data) } } @MainActor private final class WithdrawalAuditWalletMock: WalletServing { var withdrawResponses: [WalletWithdrawListResponse] = [] var withdrawError: Error? private(set) var withdrawPages: [Int] = [] func walletSummary(type: Int) async throws -> WalletSummaryResponse { WalletSummaryResponse() } func walletEarningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> WalletEarningDetailResponse { WalletEarningDetailResponse() } func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse { withdrawPages.append(page) if let withdrawError { throw withdrawError } return withdrawResponses.isEmpty ? WalletWithdrawListResponse() : withdrawResponses.removeFirst() } func withdrawInfo() async throws -> WithdrawInfoResponse { let data = #"{"amount_withdrawable":"0","min_withdraw_amount":"1","max_single_withdraw_amount":"100","max_daily_withdraw_amount":"100","user_phone":"13800000000","bank_card":{},"withdraw_info":[]}"#.data(using: .utf8)! return try JSONDecoder().decode(WithdrawInfoResponse.self, from: data) } func withdrawSendSms() async throws {} func withdrawApply(amount: String, smsCode: String) async throws {} func bankCardInfo() async throws -> BankCardInfoResponse { BankCardInfoResponse(bankCard: nil) } func bankList() async throws -> BankListResponse { BankListResponse() } func areas() async throws -> [AreaNode] { [] } func bankCardVerifyCode() async throws {} func updateBankInfo(_ request: UpdateBankInfoRequest) async throws {} func pointOverview(staffId: Int) async throws -> PointOverviewResponse { PointOverviewResponse() } func pointWithdrawApply(points: Int, remark: String) async throws {} func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse { PointWithdrawListResponse() } } private enum TestError: LocalizedError { case sample var errorDescription: String? { "测试错误" } }