// // InviteTests.swift // suixinkanTests // import XCTest @testable import suixinkan /// 邀请 API 和 ViewModel 测试。 @MainActor final class InviteTests: XCTestCase { /// 测试邀请 API path 和分页参数。 func testInviteAPIUsesExpectedRequests() async throws { let session = MockURLSession(responses: [Self.inviteInfoEnvelope, Self.inviteUsersEnvelope]) let api = InviteAPI(client: APIClient(environment: .testing, 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?.realName, "摄影师") XCTAssertEqual(session.requests.map { $0.url?.path }, [ "/api/yf-handset-app/photog/invite-info", "/api/yf-handset-app/photog/invite/user-list", ]) let query = queryItems(from: session.requests[1]) XCTAssertEqual(query["page"], "1") XCTAssertEqual(query["page_size"], "1") } /// 测试邀请信息模型宽松解码。 func testInviteInfoResponseDecodesLossyFields() throws { let json = Data(#"{"enable_invite":"1","invite_code":123,"invite_url":"https://invite.example.com","description":["规则1"]}"#.utf8) let response = try JSONDecoder().decode(InviteInfoResponse.self, from: json) XCTAssertTrue(response.enableInvite) XCTAssertEqual(response.inviteCode, "123") XCTAssertEqual(response.description, ["规则1"]) } /// 测试邀请页加载信息并生成二维码。 func testPhotographerInviteViewModelLoadsInfoAndGeneratesQRCode() async { let viewModel = PhotographerInviteViewModel() await viewModel.reload(api: FakeInviteService()) XCTAssertEqual(viewModel.inviteCode, "ABC123") XCTAssertEqual(viewModel.inviteUrl, "https://invite.example.com") XCTAssertEqual(viewModel.rules, ["规则1"]) XCTAssertNotNil(viewModel.qrImage) XCTAssertFalse(viewModel.isLoading) } /// 测试邀请记录首次加载邀请用户和钱包汇总。 func testInviteRecordLoadsInviteRowsAndSummary() async { let inviteAPI = FakeInviteService() let walletAPI = FakeWalletService() let viewModel = InviteRecordViewModel() await viewModel.loadInitial(inviteAPI: inviteAPI, walletAPI: walletAPI) XCTAssertEqual(viewModel.inviteRows.first?.title, "摄影师") XCTAssertEqual(viewModel.totalRewardText, "100.00") XCTAssertEqual(viewModel.withdrawableText, "50.00") XCTAssertEqual(walletAPI.summaryTypes, [2]) XCTAssertFalse(viewModel.canLoadMoreInvite) } /// 测试奖励 Tab 加载钱包流水并使用 type=2。 func testInviteRecordRewardTabLoadsWalletTransactions() async { let inviteAPI = FakeInviteService() let walletAPI = FakeWalletService() let viewModel = InviteRecordViewModel() await viewModel.selectTab(.reward, inviteAPI: inviteAPI, walletAPI: walletAPI) XCTAssertEqual(viewModel.selectedTab, .reward) XCTAssertEqual(viewModel.rewardRows.first?.amount, "8.00") XCTAssertEqual(walletAPI.transactionQueries.first?.type, 2) XCTAssertEqual(walletAPI.transactionQueries.first?.page, 1) } /// 测试首页菜单 catalog 包含邀请相关 URI。 func testHomeMenuCatalogContainsInviteRoutes() { XCTAssertEqual(HomeMenuCatalog.item(for: "registration_invitation")?.title, "注册邀请") XCTAssertEqual(HomeMenuCatalog.item(for: "photographer_invite")?.title, "邀请摄影师") XCTAssertEqual(HomeMenuCatalog.item(for: "invite_record")?.title, "邀请记录") } private static let inviteInfoEnvelope = Data(#"{"code":100000,"msg":"success","data":{"enable_invite":"1","invite_code":"ABC123","invite_url":"https://invite.example.com","description":["规则1"]}}"#.utf8) private static let inviteUsersEnvelope = Data(#"{"code":100000,"msg":"success","data":[{"id":"1","real_name":"摄影师","phone":"13800000000","avatar":"","created_at":"2026-06-24","invite_level":"1"}]}"#.utf8) } @MainActor /// 邀请服务测试替身。 private final class FakeInviteService: InviteServing { func inviteInfo() async throws -> InviteInfoResponse { InviteInfoResponse( enableInvite: true, inviteCode: "ABC123", inviteUrl: "https://invite.example.com", description: ["规则1"] ) } func inviteUserList(page: Int, pageSize: Int) async throws -> [InviteUserItem] { [ InviteUserItem( id: 1, realName: "摄影师", phone: "13800000000", avatar: "", createdAt: "2026-06-24", inviteLevel: 1 ), ] } } @MainActor /// 钱包服务测试替身。 private final class FakeWalletService: WalletServing { private(set) var summaryTypes: [Int] = [] private(set) var transactionQueries: [(page: Int, pageSize: Int, type: Int)] = [] func walletSummary(type: Int) async throws -> WalletSummaryResponse { summaryTypes.append(type) return WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "20.00", amountWithdrawable: "50.00") } func walletTransactionList( startDate: String, endDate: String, page: Int, pageSize: Int, type: Int ) async throws -> WalletTransactionListResponse { transactionQueries.append((page, pageSize, type)) return WalletTransactionListResponse( total: 1, list: [ WalletTransactionItem( id: 1, amount: "8.00", transactionType: 2, transactionTypeLabel: "邀请奖励", createdAt: "2026-06-24", withdrawLabel: "可提现" ), ] ) } } private func queryItems(from request: URLRequest) -> [String: String] { URLComponents(url: request.url!, resolvingAgainstBaseURL: false)? .queryItems? .reduce(into: [String: String]()) { result, item in result[item.name] = item.value } ?? [:] }