将定金订单、历史拍摄与多行程上传接入 Orders,并以真实页面替换首页排队管理、消息中心、景区结算与提现审核占位入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
597 lines
28 KiB
Swift
597 lines
28 KiB
Swift
//
|
||
// OrderLongTailTests.swift
|
||
// suixinkanTests
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import XCTest
|
||
@testable import suixinkan
|
||
|
||
@MainActor
|
||
/// 订单长尾测试,覆盖押金订单、退款和历史拍摄的 API 与 ViewModel 规则。
|
||
final class OrderLongTailTests: XCTestCase {
|
||
/// 测试押金订单接口使用旧工程路径、分页兜底和请求体。
|
||
func testDepositOrderAPIRequestsAndDecodesLossyFields() async throws {
|
||
let session = LongTailRecordingURLSession(responses: [
|
||
try TestFixture.data(named: "deposit_order_list_success"),
|
||
try TestFixture.data(named: "empty_success"),
|
||
try TestFixture.data(named: "empty_success")
|
||
])
|
||
let api = OrdersAPI(client: APIClient(session: session))
|
||
|
||
let list = try await api.depositOrderList(scenicId: 88, page: 0, pageSize: 0)
|
||
try await api.depositOrderWriteOff(orderNumber: "DEP001")
|
||
try await api.depositOrderRefund(orderNumber: "DEP002", refundReason: "游客取消行程")
|
||
|
||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||
"/api/yf-handset-app/photog/order/deposit-list",
|
||
"/api/yf-handset-app/photog/order/deposit-writeoff",
|
||
"/api/yf-handset-app/photog/order/deposit-refund"
|
||
])
|
||
let query = longTailQueryItems(from: try XCTUnwrap(session.requests.first))
|
||
XCTAssertEqual(query["scenic_id"], "88")
|
||
XCTAssertEqual(query["page"], "1")
|
||
XCTAssertEqual(query["page_size"], "1")
|
||
XCTAssertEqual(list.total, 2)
|
||
XCTAssertEqual(list.list[1].id, 4102)
|
||
XCTAssertEqual(list.list[1].userPhone, "13900000000")
|
||
|
||
let writeOffBody = try longTailBodyObject(from: session.requests[1])
|
||
XCTAssertEqual(writeOffBody["order_number"] as? String, "DEP001")
|
||
let refundBody = try longTailBodyObject(from: session.requests[2])
|
||
XCTAssertEqual(refundBody["order_number"] as? String, "DEP002")
|
||
XCTAssertEqual(refundBody["refund_reason"] as? String, "游客取消行程")
|
||
}
|
||
|
||
/// 测试拍摄信息、普通退款和历史拍摄接口 path、query、body 与宽松解码。
|
||
func testShootingRefundAndHistoryAPIRequestsAndDecodes() async throws {
|
||
let session = LongTailRecordingURLSession(responses: [
|
||
try TestFixture.data(named: "store_order_shooting_detail_success"),
|
||
try TestFixture.data(named: "multi_travel_shoot_history_success"),
|
||
try TestFixture.data(named: "empty_success")
|
||
])
|
||
let api = OrdersAPI(client: APIClient(session: session))
|
||
|
||
let shooting = try await api.storeOrderShootingDetail(
|
||
storeId: 66,
|
||
orderNumber: "DEP001",
|
||
scenicSpotId: 901,
|
||
photogUid: 42
|
||
)
|
||
let history = try await api.multiTravelShootHistory(orderNumber: "MT001")
|
||
try await api.orderRefund(
|
||
orderNumber: "STORE001",
|
||
refundType: .partial,
|
||
refundAmount: "10.50",
|
||
refundReason: "游客取消部分项目"
|
||
)
|
||
|
||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||
"/api/app/store/order/shooting-detail",
|
||
"/api/yf-handset-app/photog/order/multi-travel/shoot-history",
|
||
"/api/yf-handset-app/photog/order/refund"
|
||
])
|
||
let shootingQuery = longTailQueryItems(from: session.requests[0])
|
||
XCTAssertEqual(shootingQuery["store_id"], "66")
|
||
XCTAssertEqual(shootingQuery["order_number"], "DEP001")
|
||
XCTAssertEqual(shootingQuery["scenic_spot_id"], "901")
|
||
XCTAssertEqual(shootingQuery["photog_uid"], "42")
|
||
XCTAssertEqual(shooting.orderComment?.starShooting, 5)
|
||
XCTAssertEqual(shooting.materialList.first?.fileSize, 10_485_760)
|
||
XCTAssertTrue(shooting.materialList.first?.isVideo == true)
|
||
|
||
XCTAssertEqual(longTailQueryItems(from: session.requests[1])["order_number"], "MT001")
|
||
XCTAssertEqual(history.projectType, 19)
|
||
XCTAssertEqual(history.photogSpotList.first?.photographerDisplayName, "小景")
|
||
XCTAssertEqual(history.photogSpotList.first?.files.count, 2)
|
||
|
||
let refundBody = try longTailBodyObject(from: session.requests[2])
|
||
XCTAssertEqual(refundBody["order_number"] as? String, "STORE001")
|
||
XCTAssertEqual(refundBody["refund_type"] as? Int, OrderRefundMode.partial.rawValue)
|
||
XCTAssertEqual(refundBody["refund_amount"] as? String, "10.50")
|
||
XCTAssertNil(refundBody["store_id"])
|
||
}
|
||
|
||
/// 测试已核销打卡点和多点旅拍素材提交接口 path、query、body。
|
||
func testMultiTravelTaskUploadAPIRequestsAndDecodes() async throws {
|
||
let session = LongTailRecordingURLSession(responses: [
|
||
try TestFixture.data(named: "multi_travel_verified_spots_success"),
|
||
try TestFixture.data(named: "empty_success")
|
||
])
|
||
let api = OrdersAPI(client: APIClient(session: session))
|
||
|
||
let spots = try await api.multiTravelVerifiedScenicSpotList(orderNumber: "MT001")
|
||
try await api.multiTravelUploadMaterial(
|
||
MultiTravelUploadMaterialRequest(
|
||
orderNumber: "MT001",
|
||
scenicSpotId: 901,
|
||
cloudFile: [MultiTravelCloudFileItem(fileId: 7001)],
|
||
uploadFile: [MultiTravelUploadFileItem(fileName: "spot.jpg", fileUrl: "https://cdn.example.com/spot.jpg")]
|
||
)
|
||
)
|
||
|
||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||
"/api/yf-handset-app/photog/order/multi-travel/verified-scenic-spot-list",
|
||
"/api/yf-handset-app/photog/order/multi-travel/upload-material"
|
||
])
|
||
XCTAssertEqual(longTailQueryItems(from: session.requests[0])["order_number"], "MT001")
|
||
XCTAssertEqual(spots.map(\.id), [901, 902])
|
||
XCTAssertEqual(spots.last?.name, "观景台")
|
||
let body = try longTailBodyObject(from: session.requests[1])
|
||
XCTAssertEqual(body["order_number"] as? String, "MT001")
|
||
XCTAssertEqual(body["scenic_spot_id"] as? Int, 901)
|
||
XCTAssertEqual((body["cloud_file"] as? [[String: Any]])?.first?["file_id"] as? Int, 7001)
|
||
XCTAssertEqual((body["upload_file"] as? [[String: Any]])?.first?["file_url"] as? String, "https://cdn.example.com/spot.jpg")
|
||
}
|
||
|
||
/// 测试押金订单无景区时清空旧数据且不请求接口。
|
||
func testDepositListClearsWithoutScenicAndStopsAtLastPage() async throws {
|
||
let service = LongTailMockOrderService()
|
||
service.depositListResponses = [try Self.depositPayload()]
|
||
let viewModel = DepositOrderListViewModel()
|
||
|
||
await viewModel.reload(api: service, scenicId: 88, reset: true)
|
||
await viewModel.reload(api: service, scenicId: 88, reset: false)
|
||
await viewModel.reload(api: service, scenicId: nil, reset: true)
|
||
|
||
XCTAssertTrue(viewModel.orders.isEmpty)
|
||
XCTAssertEqual(viewModel.total, 0)
|
||
XCTAssertEqual(viewModel.page, 1)
|
||
XCTAssertEqual(service.depositListCalls, [
|
||
.init(scenicId: 88, page: 1, pageSize: 10)
|
||
])
|
||
}
|
||
|
||
/// 测试押金核销和押金退款成功后刷新列表,失败时保留旧列表并清理操作状态。
|
||
func testDepositWriteOffAndRefundRefreshOrKeepListOnFailure() async throws {
|
||
let service = LongTailMockOrderService()
|
||
service.depositListResponses = [
|
||
try Self.depositPayload(),
|
||
try Self.depositPayload(),
|
||
try Self.depositPayload()
|
||
]
|
||
let viewModel = DepositOrderListViewModel()
|
||
await viewModel.reload(api: service, scenicId: 88, reset: true)
|
||
|
||
let writeOffSuccess = await viewModel.writeOff(api: service, scenicId: 88, orderNumber: "DEP001")
|
||
let emptyReason = await viewModel.refund(api: service, scenicId: 88, orderNumber: "DEP001", reason: " ")
|
||
service.depositRefundError = APIError.serverCode(500, "退款失败")
|
||
let refundFailure = await viewModel.refund(api: service, scenicId: 88, orderNumber: "DEP001", reason: "游客取消")
|
||
|
||
XCTAssertTrue(writeOffSuccess)
|
||
XCTAssertFalse(emptyReason)
|
||
XCTAssertFalse(refundFailure)
|
||
XCTAssertEqual(service.depositWriteOffNumbers, ["DEP001"])
|
||
XCTAssertEqual(service.depositRefundCalls, [.init(orderNumber: "DEP001", reason: "游客取消")])
|
||
XCTAssertEqual(service.depositListCalls.count, 2)
|
||
XCTAssertEqual(viewModel.orders.count, 2)
|
||
XCTAssertNil(viewModel.operatingOrderNumber)
|
||
XCTAssertEqual(viewModel.errorMessage, "退款失败")
|
||
}
|
||
|
||
/// 测试押金详情和拍摄信息缺少门店时不请求接口。
|
||
func testDepositDetailAndShootingSkipWithoutStore() async {
|
||
let service = LongTailMockOrderService()
|
||
let detailViewModel = DepositOrderDetailViewModel()
|
||
let shootingViewModel = DepositOrderShootingInfoViewModel()
|
||
|
||
await detailViewModel.load(api: service, storeId: nil, orderNumber: "DEP001")
|
||
await shootingViewModel.load(api: service, storeId: nil, orderNumber: "DEP001", scenicSpotId: 901, photogUid: 42)
|
||
|
||
XCTAssertTrue(service.storeDetailCalls.isEmpty)
|
||
XCTAssertTrue(service.storeShootingCalls.isEmpty)
|
||
XCTAssertEqual(detailViewModel.errorMessage, "当前账号缺少门店信息")
|
||
XCTAssertEqual(shootingViewModel.errorMessage, "当前账号缺少门店信息")
|
||
}
|
||
|
||
/// 测试普通退款入口规则、金额校验、重复提交保护和成功提交参数。
|
||
func testOrderRefundValidationAndSubmit() async throws {
|
||
let service = LongTailMockOrderService()
|
||
let viewModel = OrderRefundViewModel()
|
||
let refundable = try Self.orderEntity(orderStatus: 18, orderType: 4, actualPayAmount: "99.50")
|
||
let unavailable = try Self.orderEntity(orderStatus: 50, orderType: 4, actualPayAmount: "99.50")
|
||
|
||
XCTAssertTrue(viewModel.canRefund(refundable))
|
||
XCTAssertFalse(viewModel.canRefund(unavailable))
|
||
viewModel.begin(item: refundable)
|
||
XCTAssertEqual(viewModel.amount, "99.50")
|
||
let missingReasonSuccess = await viewModel.submit(api: service, item: refundable)
|
||
XCTAssertFalse(missingReasonSuccess)
|
||
XCTAssertEqual(viewModel.errorMessage, "请输入退款原因")
|
||
|
||
viewModel.reason = "部分退款"
|
||
viewModel.mode = .partial
|
||
viewModel.amount = "12.345"
|
||
let invalidAmountSuccess = await viewModel.submit(api: service, item: refundable)
|
||
XCTAssertFalse(invalidAmountSuccess)
|
||
XCTAssertEqual(viewModel.errorMessage, "请输入有效的退款金额")
|
||
|
||
viewModel.amount = "120.00"
|
||
let overLimitSuccess = await viewModel.submit(api: service, item: refundable)
|
||
XCTAssertFalse(overLimitSuccess)
|
||
XCTAssertEqual(viewModel.errorMessage, "退款金额不能大于可退金额")
|
||
|
||
viewModel.amount = "12.3"
|
||
let submitSuccess = await viewModel.submit(api: service, item: refundable)
|
||
XCTAssertTrue(submitSuccess)
|
||
XCTAssertEqual(service.orderRefundCalls, [
|
||
.init(orderNumber: "STORE001", mode: .partial, amount: "12.30", reason: "部分退款")
|
||
])
|
||
XCTAssertNil(viewModel.errorMessage)
|
||
XCTAssertFalse(viewModel.submitting)
|
||
}
|
||
|
||
/// 测试历史拍摄空订单不请求,成功后展示项目和媒体,失败后清空旧数据。
|
||
func testHistoricalShootingLoadSuccessAndFailure() async throws {
|
||
let service = LongTailMockOrderService()
|
||
service.historyResponses = [try TestFixture.payload(MultiTravelShootHistoryResponse.self, named: "multi_travel_shoot_history_success")]
|
||
let viewModel = HistoricalShootingInfoViewModel()
|
||
|
||
await viewModel.load(api: service, orderNumber: " ")
|
||
XCTAssertTrue(service.historyCalls.isEmpty)
|
||
XCTAssertEqual(viewModel.errorMessage, "订单号不能为空")
|
||
|
||
await viewModel.load(api: service, orderNumber: "MT001")
|
||
XCTAssertEqual(viewModel.projectName, "多点旅拍")
|
||
XCTAssertEqual(viewModel.spots.first?.scenicSpotName, "湖心亭")
|
||
XCTAssertEqual(viewModel.spots.first?.files.count, 2)
|
||
|
||
service.historyError = APIError.serverCode(500, "历史拍摄失败")
|
||
await viewModel.load(api: service, orderNumber: "MT001")
|
||
XCTAssertTrue(viewModel.spots.isEmpty)
|
||
XCTAssertEqual(viewModel.errorMessage, "历史拍摄失败")
|
||
}
|
||
|
||
/// 测试任务上传空订单不请求,加载成功默认选中首个打卡点,加载失败清空状态。
|
||
func testMultiTravelTaskUploadLoadsAndRepairsSpotSelection() async throws {
|
||
let service = LongTailMockOrderService()
|
||
service.verifiedSpotResponses = [
|
||
try Self.verifiedSpots(),
|
||
[try Self.verifiedSpot(id: 902, name: "观景台")]
|
||
]
|
||
let viewModel = MultiTravelTaskUploadViewModel()
|
||
|
||
await viewModel.loadSpots(api: service)
|
||
XCTAssertTrue(service.verifiedSpotCalls.isEmpty)
|
||
|
||
viewModel.orderNumber = "MT001"
|
||
await viewModel.loadSpots(api: service)
|
||
XCTAssertEqual(viewModel.selectedSpotId, 901)
|
||
viewModel.selectSpot(id: 901)
|
||
await viewModel.loadSpots(api: service)
|
||
XCTAssertEqual(viewModel.selectedSpotId, 902)
|
||
|
||
service.verifiedSpotError = APIError.serverCode(500, "打卡点失败")
|
||
await viewModel.loadSpots(api: service)
|
||
XCTAssertTrue(viewModel.spots.isEmpty)
|
||
XCTAssertNil(viewModel.selectedSpotId)
|
||
XCTAssertEqual(viewModel.errorMessage, "打卡点失败")
|
||
}
|
||
|
||
/// 测试任务上传提交校验和仅云盘文件直接提交。
|
||
func testMultiTravelTaskUploadValidatesAndSubmitsCloudFiles() async throws {
|
||
let service = LongTailMockOrderService()
|
||
service.verifiedSpotResponses = [try Self.verifiedSpots()]
|
||
let uploader = LongTailMockUploader()
|
||
let viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: "MT001")
|
||
|
||
let missingScenicSuccess = await viewModel.submit(api: service, uploadService: uploader, scenicId: nil)
|
||
XCTAssertFalse(missingScenicSuccess)
|
||
XCTAssertEqual(viewModel.errorMessage, "请先选择景区")
|
||
|
||
await viewModel.loadSpots(api: service)
|
||
let missingFileSuccess = await viewModel.submit(api: service, uploadService: uploader, scenicId: 88)
|
||
XCTAssertFalse(missingFileSuccess)
|
||
XCTAssertEqual(viewModel.errorMessage, "请至少选择一个素材文件")
|
||
|
||
viewModel.mergeCloudFiles([TaskCloudSelectionItem(id: 7001, fileName: "cloud.jpg", fileType: 2, remark: "")])
|
||
let submitSuccess = await viewModel.submit(api: service, uploadService: uploader, scenicId: 88)
|
||
|
||
XCTAssertTrue(submitSuccess)
|
||
XCTAssertTrue(viewModel.didSubmitSuccessfully)
|
||
XCTAssertEqual(service.uploadMaterialRequests, [
|
||
MultiTravelUploadMaterialRequest(
|
||
orderNumber: "MT001",
|
||
scenicSpotId: 901,
|
||
cloudFile: [MultiTravelCloudFileItem(fileId: 7001)],
|
||
uploadFile: []
|
||
)
|
||
])
|
||
XCTAssertTrue(uploader.uploadTaskFiles.isEmpty)
|
||
}
|
||
|
||
/// 测试任务上传本地文件先上传 OSS,上传失败不提交业务接口。
|
||
func testMultiTravelTaskUploadUploadsLocalFilesBeforeSubmit() async throws {
|
||
let service = LongTailMockOrderService()
|
||
service.verifiedSpotResponses = [try Self.verifiedSpots()]
|
||
let uploader = LongTailMockUploader()
|
||
uploader.uploadTaskFileURLs = ["https://cdn.example.com/local.jpg"]
|
||
let viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: "MT001")
|
||
await viewModel.loadSpots(api: service)
|
||
viewModel.addLocalFile(data: Data([1, 2, 3]), fileName: "local.jpg")
|
||
|
||
let success = await viewModel.submit(api: service, uploadService: uploader, scenicId: 88)
|
||
|
||
XCTAssertTrue(success)
|
||
XCTAssertEqual(uploader.uploadTaskFiles.map(\.fileName), ["local.jpg"])
|
||
XCTAssertEqual(service.uploadMaterialRequests.first?.uploadFile, [
|
||
MultiTravelUploadFileItem(fileName: "local.jpg", fileUrl: "https://cdn.example.com/local.jpg")
|
||
])
|
||
|
||
let failingService = LongTailMockOrderService()
|
||
failingService.verifiedSpotResponses = [try Self.verifiedSpots()]
|
||
let failingUploader = LongTailMockUploader()
|
||
failingUploader.uploadError = APIError.serverCode(500, "上传失败")
|
||
let failingViewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: "MT001")
|
||
await failingViewModel.loadSpots(api: failingService)
|
||
failingViewModel.addLocalFile(data: Data([1]), fileName: "bad.mp4")
|
||
|
||
let failure = await failingViewModel.submit(api: failingService, uploadService: failingUploader, scenicId: 88)
|
||
|
||
XCTAssertFalse(failure)
|
||
XCTAssertTrue(failingService.uploadMaterialRequests.isEmpty)
|
||
XCTAssertEqual(failingViewModel.errorMessage, "上传失败")
|
||
}
|
||
|
||
/// 读取押金订单列表 fixture。
|
||
private static func depositPayload() throws -> ListPayload<DepositOrderListItem> {
|
||
try TestFixture.payload(ListPayload<DepositOrderListItem>.self, named: "deposit_order_list_success")
|
||
}
|
||
|
||
/// 构建最小普通订单实体,用于退款规则测试。
|
||
private static func orderEntity(orderStatus: Int, orderType: Int, actualPayAmount: String) throws -> OrderEntity {
|
||
let json = """
|
||
{
|
||
"order_number": "STORE001",
|
||
"order_status": "\(orderStatus)",
|
||
"order_type": "\(orderType)",
|
||
"actual_pay_amount": "\(actualPayAmount)",
|
||
"actual_refund_amount": "0",
|
||
"refund_amount": "0",
|
||
"order_type_label": "旅拍订单",
|
||
"order_status_name": "待退款"
|
||
}
|
||
"""
|
||
return try JSONDecoder().decode(OrderEntity.self, from: Data(json.utf8))
|
||
}
|
||
|
||
/// 读取已核销打卡点 fixture。
|
||
private static func verifiedSpots() throws -> [MultiTravelVerifiedScenicSpotItem] {
|
||
try TestFixture.payload([MultiTravelVerifiedScenicSpotItem].self, named: "multi_travel_verified_spots_success")
|
||
}
|
||
|
||
/// 构建已核销打卡点测试实体。
|
||
private static func verifiedSpot(id: Int, name: String) throws -> MultiTravelVerifiedScenicSpotItem {
|
||
let json = #"{"id":"\#(id)","name":"\#(name)"}"#
|
||
return try JSONDecoder().decode(MultiTravelVerifiedScenicSpotItem.self, from: Data(json.utf8))
|
||
}
|
||
}
|
||
|
||
/// 长尾订单 URLSession 测试替身,按调用顺序返回响应并记录请求。
|
||
private final class LongTailRecordingURLSession: URLSessionProtocol {
|
||
private var responses: [Data]
|
||
private(set) var requests: [URLRequest] = []
|
||
|
||
init(responses: [Data]) {
|
||
self.responses = responses
|
||
}
|
||
|
||
/// 返回队列中的下一个响应。
|
||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||
requests.append(request)
|
||
let data = responses.isEmpty ? Data(#"{"code":100000,"msg":"success"}"#.utf8) : responses.removeFirst()
|
||
return (
|
||
data,
|
||
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||
)
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 长尾订单服务测试替身,记录押金、退款和历史拍摄调用。
|
||
private final class LongTailMockOrderService: OrderServing {
|
||
struct DepositListCall: Equatable {
|
||
let scenicId: Int
|
||
let page: Int
|
||
let pageSize: Int
|
||
}
|
||
|
||
struct DepositRefundCall: Equatable {
|
||
let orderNumber: String
|
||
let reason: String
|
||
}
|
||
|
||
struct OrderRefundCall: Equatable {
|
||
let orderNumber: String
|
||
let mode: OrderRefundMode
|
||
let amount: String
|
||
let reason: String
|
||
}
|
||
|
||
var depositListResponses: [ListPayload<DepositOrderListItem>] = []
|
||
var detailResponses: [StoreOrderDetailResponse] = []
|
||
var shootingResponses: [StoreOrderShootingDetailResponse] = []
|
||
var historyResponses: [MultiTravelShootHistoryResponse] = []
|
||
var verifiedSpotResponses: [[MultiTravelVerifiedScenicSpotItem]] = []
|
||
var depositWriteOffError: Error?
|
||
var depositRefundError: Error?
|
||
var orderRefundError: Error?
|
||
var historyError: Error?
|
||
var verifiedSpotError: Error?
|
||
var uploadMaterialError: Error?
|
||
|
||
private(set) var depositListCalls: [DepositListCall] = []
|
||
private(set) var depositWriteOffNumbers: [String] = []
|
||
private(set) var depositRefundCalls: [DepositRefundCall] = []
|
||
private(set) var orderRefundCalls: [OrderRefundCall] = []
|
||
private(set) var storeDetailCalls: [(storeId: Int, orderNumber: String)] = []
|
||
private(set) var storeShootingCalls: [(storeId: Int, orderNumber: String, scenicSpotId: Int, photogUid: Int)] = []
|
||
private(set) var historyCalls: [String] = []
|
||
private(set) var verifiedSpotCalls: [String] = []
|
||
private(set) var uploadMaterialRequests: [MultiTravelUploadMaterialRequest] = []
|
||
|
||
/// 基础订单列表接口在长尾测试中不使用。
|
||
func orderList(
|
||
scenicId: Int,
|
||
page: Int,
|
||
pageSize: Int,
|
||
orderStatus: Int?,
|
||
userPhone: String?,
|
||
startTime: String?,
|
||
endTime: String?,
|
||
isRefined: Int?,
|
||
isScenicAdmin: Bool
|
||
) async throws -> ListPayload<OrderEntity> {
|
||
ListPayload(total: 0, list: [])
|
||
}
|
||
|
||
/// 核销列表接口在长尾测试中不使用。
|
||
func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
|
||
ListPayload(total: 0, list: [])
|
||
}
|
||
|
||
/// 普通核销接口在长尾测试中不使用。
|
||
func writeOff(orderNumber: String) async throws {}
|
||
|
||
/// 返回门店订单详情。
|
||
func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse {
|
||
storeDetailCalls.append((storeId: storeId, orderNumber: orderNumber))
|
||
return detailResponses.isEmpty ? try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success") : detailResponses.removeFirst()
|
||
}
|
||
|
||
/// 返回押金订单列表。
|
||
func depositOrderList(scenicId: Int, page: Int, pageSize: Int) async throws -> ListPayload<DepositOrderListItem> {
|
||
depositListCalls.append(.init(scenicId: scenicId, page: page, pageSize: pageSize))
|
||
return depositListResponses.isEmpty ? ListPayload(total: 0, list: []) : depositListResponses.removeFirst()
|
||
}
|
||
|
||
/// 记录押金核销订单号。
|
||
func depositOrderWriteOff(orderNumber: String) async throws {
|
||
depositWriteOffNumbers.append(orderNumber)
|
||
if let depositWriteOffError {
|
||
throw depositWriteOffError
|
||
}
|
||
}
|
||
|
||
/// 记录押金退款参数。
|
||
func depositOrderRefund(orderNumber: String, refundReason: String) async throws {
|
||
depositRefundCalls.append(.init(orderNumber: orderNumber, reason: refundReason))
|
||
if let depositRefundError {
|
||
throw depositRefundError
|
||
}
|
||
}
|
||
|
||
/// 返回拍摄信息详情。
|
||
func storeOrderShootingDetail(
|
||
storeId: Int,
|
||
orderNumber: String,
|
||
scenicSpotId: Int,
|
||
photogUid: Int
|
||
) async throws -> StoreOrderShootingDetailResponse {
|
||
storeShootingCalls.append((storeId: storeId, orderNumber: orderNumber, scenicSpotId: scenicSpotId, photogUid: photogUid))
|
||
return shootingResponses.isEmpty ? try TestFixture.payload(StoreOrderShootingDetailResponse.self, named: "store_order_shooting_detail_success") : shootingResponses.removeFirst()
|
||
}
|
||
|
||
/// 记录普通退款参数。
|
||
func orderRefund(orderNumber: String, refundType: OrderRefundMode, refundAmount: String, refundReason: String) async throws {
|
||
orderRefundCalls.append(.init(orderNumber: orderNumber, mode: refundType, amount: refundAmount, reason: refundReason))
|
||
if let orderRefundError {
|
||
throw orderRefundError
|
||
}
|
||
}
|
||
|
||
/// 返回历史拍摄信息。
|
||
func multiTravelShootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
|
||
historyCalls.append(orderNumber)
|
||
if let historyError {
|
||
throw historyError
|
||
}
|
||
return historyResponses.isEmpty ? try TestFixture.payload(MultiTravelShootHistoryResponse.self, named: "multi_travel_shoot_history_success") : historyResponses.removeFirst()
|
||
}
|
||
|
||
/// 返回已核销打卡点列表。
|
||
func multiTravelVerifiedScenicSpotList(orderNumber: String) async throws -> [MultiTravelVerifiedScenicSpotItem] {
|
||
verifiedSpotCalls.append(orderNumber)
|
||
if let verifiedSpotError {
|
||
throw verifiedSpotError
|
||
}
|
||
return verifiedSpotResponses.isEmpty ? [] : verifiedSpotResponses.removeFirst()
|
||
}
|
||
|
||
/// 记录多点旅拍素材提交请求。
|
||
func multiTravelUploadMaterial(_ request: MultiTravelUploadMaterialRequest) async throws {
|
||
uploadMaterialRequests.append(request)
|
||
if let uploadMaterialError {
|
||
throw uploadMaterialError
|
||
}
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 长尾订单 OSS 上传测试替身,只记录任务附件上传。
|
||
private final class LongTailMockUploader: OSSUploadServing {
|
||
struct UploadTaskFileCall: Equatable {
|
||
let fileName: String
|
||
let fileType: Int
|
||
let scenicId: Int
|
||
let dataSize: Int
|
||
}
|
||
|
||
var uploadTaskFileURLs: [String] = []
|
||
var uploadError: Error?
|
||
private(set) var uploadTaskFiles: [UploadTaskFileCall] = []
|
||
|
||
/// 返回预设任务附件 URL。
|
||
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||
uploadTaskFiles.append(.init(fileName: fileName, fileType: fileType, scenicId: scenicId, dataSize: data.count))
|
||
if let uploadError {
|
||
throw uploadError
|
||
}
|
||
onProgress(100)
|
||
return uploadTaskFileURLs.isEmpty ? "https://cdn.example.com/\(fileName)" : uploadTaskFileURLs.removeFirst()
|
||
}
|
||
|
||
/// 其他上传入口在本测试中不使用。
|
||
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 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 { "" }
|
||
|
||
/// 其他上传入口在本测试中不使用。
|
||
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
|
||
}
|
||
|
||
/// 提取请求 query 方便断言。
|
||
private func longTailQueryItems(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) }
|
||
})
|
||
}
|
||
|
||
/// 将 JSON body 解成字典方便断言。
|
||
private func longTailBodyObject(from request: URLRequest) throws -> [String: Any] {
|
||
let data = try XCTUnwrap(request.httpBody)
|
||
let object = try JSONSerialization.jsonObject(with: data)
|
||
return try XCTUnwrap(object as? [String: Any])
|
||
}
|