Files
suixinkan_ios_new/suixinkanTests/Invite/InviteTests.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

155 lines
6.7 KiB
Swift

//
// InviteTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/24.
//
import XCTest
@testable import suixinkan
@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) }
})
}