Files
suixinkan_ios_new/suixinkanTests/WalletViewModelTests.swift
汉秋 fb8430889b Add Projects, Schedule, and Invite modules with home routing.
Migrate project management, schedule management, and photographer invitation flows from placeholders, including project image OSS upload and shared business models.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 15:57:15 +08:00

224 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// WalletViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class WalletViewModelTests: XCTestCase {
///
func testWalletInitialLoadFetchesSummaryAndEarnings() async {
let api = WalletMockAPI()
api.summary = WalletSummaryResponse(amountTotal: "100", amountCurrentBalance: "80", amountWithdrawable: "60")
api.earningResponses = [WalletEarningDetailResponse(total: 1, list: [WalletEarningDetailGroup(date: "2026-06-22")])]
let viewModel = WalletViewModel()
await viewModel.loadInitial(api: api, staffId: 7)
XCTAssertEqual(viewModel.withdrawableText, "¥ 60")
XCTAssertEqual(api.pointOverviewStaffIds, [7])
XCTAssertEqual(api.earningQueries.count, 1)
XCTAssertEqual(viewModel.earningsGroups.map(\.date), ["2026-06-22"])
}
/// Tab
func testSelectWithdrawTabLoadsWithdrawRecords() async {
let api = WalletMockAPI()
let viewModel = WalletViewModel()
await viewModel.selectTab(.withdraws, api: api)
XCTAssertEqual(api.withdrawListPages, [1])
}
///
func testWithdrawDecisionRoutesByRealNameAndBankCardState() async throws {
let profile = WalletRealNameMockAPI()
let wallet = WalletMockAPI()
let viewModel = WalletViewModel()
profile.response = RealNameInfoResponse(realNameInfo: nil)
var decision = await viewModel.resolveWithdrawDecision(profileAPI: profile, walletAPI: wallet)
XCTAssertEqual(decision, .route(.realNameAuth))
profile.response = try Self.realNameInfo(status: 2)
wallet.bankCardResponse = BankCardInfoResponse(bankCard: nil)
decision = await viewModel.resolveWithdrawDecision(profileAPI: profile, walletAPI: wallet)
XCTAssertEqual(decision, .route(.bankCardSettings))
wallet.bankCardResponse = try BankCardInfoResponse(bankCard: Self.bankCard(status: 2))
decision = await viewModel.resolveWithdrawDecision(profileAPI: profile, walletAPI: wallet)
XCTAssertEqual(decision, .route(.withdrawApply))
}
///
func testWithdrawApplySubmitsNormalizedAmount() async throws {
let api = WalletMockAPI()
api.withdrawInfoResponse = try Self.withdrawInfo(withdrawable: "100")
let viewModel = WithdrawApplyViewModel()
await viewModel.load(api: api)
viewModel.amountText = "12.345"
viewModel.smsCode = "8899"
let success = await viewModel.submit(api: api)
XCTAssertTrue(success)
XCTAssertEqual(api.withdrawApplyBodies.map(\.amount), ["12.35"])
XCTAssertEqual(api.withdrawApplyBodies.map(\.smsCode), ["8899"])
}
/// URL
func testWithdrawalSettingsUploadsImagesBeforeSubmit() async {
let api = WalletMockAPI()
let uploader = WalletUploadMock()
let viewModel = WithdrawalSettingsViewModel()
viewModel.realName = "张三"
viewModel.cardNumber = "622200"
viewModel.bankName = "中国银行"
viewModel.branchName = "杭州支行"
viewModel.provinceCode = "330000"
viewModel.cityCode = "330100"
viewModel.smsCode = "1234"
viewModel.frontImageData = Data([1])
viewModel.backImageData = Data([2])
let success = await viewModel.submit(api: api, uploader: uploader, scenicId: 9)
XCTAssertTrue(success)
XCTAssertEqual(uploader.bankUploads.count, 2)
XCTAssertEqual(api.updateBankRequests.first?.frontUrl, "https://cdn.example.com/bank_card_front.jpg")
XCTAssertEqual(api.updateBankRequests.first?.backUrl, "https://cdn.example.com/bank_card_back.jpg")
}
///
func testPointsRedemptionValidationAndSubmit() async {
let api = WalletMockAPI()
api.pointOverviewResponse = PointOverviewResponse(withdrawnPoints: 100)
let viewModel = PointsRedemptionViewModel()
await viewModel.load(api: api, staffId: 7)
viewModel.pointsText = "120"
var success = await viewModel.submit(api: api, staffId: 7)
XCTAssertFalse(success)
XCTAssertEqual(viewModel.errorMessage, "兑换积分不能超过可兑换积分")
viewModel.pointsText = "80"
success = await viewModel.submit(api: api, staffId: 7)
XCTAssertTrue(success)
XCTAssertEqual(api.pointApplyBodies.map(\.points), [80])
}
/// JSON
private static func realNameInfo(status: Int) throws -> RealNameInfoResponse {
let data = """
{"real_name_info":{"real_name":"","id_card_no":"11010519491231002X","audit_status":\(status),"is_long_valid":1}}
""".data(using: .utf8)!
return try JSONDecoder().decode(RealNameInfoResponse.self, from: data)
}
/// JSON
private static func bankCard(status: Int) throws -> WalletBankCardInfo {
let data = """
{"real_name":"","bank_name":"","branch_name":"","card_number":"622200","audit_status":\(status),"audit_status_label":""}
""".data(using: .utf8)!
return try JSONDecoder().decode(WalletBankCardInfo.self, from: data)
}
/// JSON
fileprivate static func withdrawInfo(withdrawable: String) throws -> WithdrawInfoResponse {
let data = """
{"amount_withdrawable":"\(withdrawable)","min_withdraw_amount":"1","max_single_withdraw_amount":"1000","max_daily_withdraw_amount":"1000","user_phone":"13800000000","bank_card":{"real_name":"","bank_name":"","card_number":"622200"},"withdraw_info":[]}
""".data(using: .utf8)!
return try JSONDecoder().decode(WithdrawInfoResponse.self, from: data)
}
}
@MainActor
/// API
private final class WalletMockAPI: WalletServing {
var summary = WalletSummaryResponse()
var earningResponses: [WalletEarningDetailResponse] = []
var withdrawResponses: [WalletWithdrawListResponse] = []
var withdrawInfoResponse: WithdrawInfoResponse?
var bankCardResponse = BankCardInfoResponse(bankCard: nil)
var pointOverviewResponse = PointOverviewResponse()
private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
private(set) var withdrawListPages: [Int] = []
private(set) var withdrawApplyBodies: [(amount: String, smsCode: String)] = []
private(set) var updateBankRequests: [UpdateBankInfoRequest] = []
private(set) var pointOverviewStaffIds: [Int] = []
private(set) var pointApplyBodies: [(points: Int, remark: String)] = []
func walletSummary(type: Int) async throws -> WalletSummaryResponse { summary }
func walletEarningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> WalletEarningDetailResponse {
earningQueries.append((startDate, endDate, page))
return earningResponses.isEmpty ? WalletEarningDetailResponse() : earningResponses.removeFirst()
}
func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse {
withdrawListPages.append(page)
return withdrawResponses.isEmpty ? WalletWithdrawListResponse() : withdrawResponses.removeFirst()
}
func withdrawInfo() async throws -> WithdrawInfoResponse {
if let withdrawInfoResponse { return withdrawInfoResponse }
return try WalletViewModelTests.withdrawInfo(withdrawable: "0")
}
func withdrawSendSms() async throws {}
func withdrawApply(amount: String, smsCode: String) async throws {
withdrawApplyBodies.append((amount, smsCode))
}
func bankCardInfo() async throws -> BankCardInfoResponse { bankCardResponse }
func bankList() async throws -> BankListResponse { BankListResponse(banks: ["中国银行"]) }
func areas() async throws -> [AreaNode] { [] }
func bankCardVerifyCode() async throws {}
func updateBankInfo(_ request: UpdateBankInfoRequest) async throws {
updateBankRequests.append(request)
}
func pointOverview(staffId: Int) async throws -> PointOverviewResponse {
pointOverviewStaffIds.append(staffId)
return pointOverviewResponse
}
func pointWithdrawApply(points: Int, remark: String) async throws {
pointApplyBodies.append((points, remark))
}
func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse {
PointWithdrawListResponse()
}
}
@MainActor
///
private final class WalletRealNameMockAPI: WalletRealNameServing {
var response = RealNameInfoResponse(realNameInfo: nil)
///
func realNameInfo() async throws -> RealNameInfoResponse {
response
}
}
@MainActor
///
private final class WalletUploadMock: OSSUploadServing {
private(set) var bankUploads: [String] = []
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
bankUploads.append(fileName)
onProgress(100)
return "https://cdn.example.com/\(fileName)"
}
func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
}