Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,223 @@
//
// WalletViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan_ios
@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 { "" }
}