feat: add WeChat sharing and invite flow
This commit is contained in:
161
suixinkanTests/InviteTests.swift
Normal file
161
suixinkanTests/InviteTests.swift
Normal file
@ -0,0 +1,161 @@
|
||||
//
|
||||
// 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
|
||||
} ?? [:]
|
||||
}
|
||||
Reference in New Issue
Block a user