Files
suixinkan_ios_new/suixinkanTests/OrdersViewModelTests.swift

344 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrdersViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class OrdersViewModelTests: XCTestCase {
///
func testStoreOrdersClearStaleDataWhenScenicIsMissing() async throws {
let api = MockOrderService()
api.storeResponses = [try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: nil)
XCTAssertFalse(viewModel.storeOrders.isEmpty)
try await viewModel.reloadStoreOrders(api: api, scenicId: nil, roleId: nil)
XCTAssertTrue(viewModel.storeOrders.isEmpty)
XCTAssertEqual(viewModel.storeTotal, 0)
XCTAssertEqual(viewModel.storePage, 1)
XCTAssertFalse(viewModel.storeHasMore)
XCTAssertEqual(api.orderListCalls.count, 1)
}
///
func testStoreOrdersDoNotRequestMoreAfterLastPage() async throws {
let api = MockOrderService()
api.storeResponses = [try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: nil)
try await viewModel.loadMoreStoreOrders(api: api, scenicId: 88, roleId: nil)
XCTAssertEqual(viewModel.storeOrders.count, 2)
XCTAssertEqual(viewModel.storeTotal, 2)
XCTAssertEqual(viewModel.storePage, 1)
XCTAssertFalse(viewModel.storeHasMore)
XCTAssertEqual(api.orderListCalls.count, 1)
}
///
func testStoreOrderFilterUsesExpectedQueries() async throws {
let api = MockOrderService()
api.storeResponses = [
try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success"),
try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")
]
let viewModel = OrdersViewModel()
viewModel.selectedStatus = -1
viewModel.searchPhone = " 13800000000 "
viewModel.filterStartDate = Self.date("2026-05-01")
viewModel.filterEndDate = Self.date("2026-05-31")
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: nil)
viewModel.selectedStatus = 30
viewModel.searchPhone = " "
viewModel.filterStartDate = nil
viewModel.filterEndDate = nil
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: 53)
XCTAssertEqual(api.orderListCalls.count, 2)
XCTAssertEqual(api.orderListCalls[0].page, 1)
XCTAssertEqual(api.orderListCalls[0].isRefined, 1)
XCTAssertEqual(api.orderListCalls[0].userPhone, "13800000000")
XCTAssertNil(api.orderListCalls[0].orderStatus)
XCTAssertEqual(api.orderListCalls[0].startTime, "2026-05-01")
XCTAssertEqual(api.orderListCalls[0].endTime, "2026-05-31")
XCTAssertFalse(api.orderListCalls[0].isScenicAdmin)
XCTAssertEqual(api.orderListCalls[1].orderStatus, 30)
XCTAssertNil(api.orderListCalls[1].isRefined)
XCTAssertNil(api.orderListCalls[1].userPhone)
XCTAssertTrue(api.orderListCalls[1].isScenicAdmin)
XCTAssertEqual(viewModel.storePage, 1)
}
///
func testWriteOffOrdersClearStaleDataWhenScenicIsMissing() async throws {
let api = MockOrderService()
api.writeOffResponses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadWriteOffOrders(api: api, scenicId: 88, storeId: 66)
XCTAssertFalse(viewModel.writeOffOrders.isEmpty)
try await viewModel.reloadWriteOffOrders(api: api, scenicId: nil)
XCTAssertTrue(viewModel.writeOffOrders.isEmpty)
XCTAssertEqual(viewModel.writeOffTotal, 0)
XCTAssertEqual(viewModel.writeOffPage, 1)
XCTAssertFalse(viewModel.writeOffHasMore)
XCTAssertEqual(api.writeOffListCalls.count, 1)
XCTAssertEqual(api.writeOffListCalls[0].storeId, 66)
}
///
func testWriteOffOrdersDoNotRequestMoreAfterLastPage() async throws {
let api = MockOrderService()
api.writeOffResponses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadWriteOffOrders(api: api, scenicId: 88)
try await viewModel.loadMoreWriteOffOrders(api: api, scenicId: 88)
XCTAssertEqual(viewModel.writeOffOrders.count, 1)
XCTAssertEqual(viewModel.writeOffTotal, 1)
XCTAssertEqual(viewModel.writeOffPage, 1)
XCTAssertFalse(viewModel.writeOffHasMore)
XCTAssertEqual(api.writeOffListCalls.count, 1)
}
///
func testVerifyRefreshesWriteOffOrdersAndResetsPagingState() async throws {
let api = MockOrderService()
api.writeOffResponses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
let viewModel = OrdersViewModel()
try await viewModel.verify(api: api, scenicId: 88, storeId: 66, orderNumber: "VERIFY001")
XCTAssertEqual(api.verifiedOrderNumbers, ["VERIFY001"])
XCTAssertEqual(api.writeOffListCalls.count, 1)
XCTAssertEqual(api.writeOffListCalls[0].scenicId, 88)
XCTAssertEqual(api.writeOffListCalls[0].storeId, 66)
XCTAssertEqual(api.writeOffListCalls[0].page, 1)
XCTAssertEqual(viewModel.writeOffOrders.count, 1)
XCTAssertEqual(viewModel.writeOffPage, 1)
XCTAssertFalse(viewModel.isVerifying)
XCTAssertNil(viewModel.currentVerifyingOrderNumber)
}
///
func testScanResultMatchesCurrentWriteOffOrder() async throws {
let api = MockOrderService()
api.writeOffResponses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadWriteOffOrders(api: api, scenicId: 88)
let result = try XCTUnwrap(viewModel.matchedWriteOffOrder(for: "https://example.com/scan?order_number=VERIFY001"))
XCTAssertEqual(result.orderNumber, "VERIFY001")
XCTAssertEqual(result.matched?.orderNumber, "VERIFY001")
}
///
func testScanResultKeepsParsedOrderNumberWhenCurrentListDoesNotContainIt() async throws {
let api = MockOrderService()
api.writeOffResponses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadWriteOffOrders(api: api, scenicId: 88)
let result = try XCTUnwrap(viewModel.matchedWriteOffOrder(for: "order-number=VERIFY999"))
XCTAssertEqual(result.orderNumber, "VERIFY999")
XCTAssertNil(result.matched)
}
///
func testVerifyIgnoresDuplicateSubmitWhileAlreadyVerifying() async throws {
let api = MockOrderService()
let viewModel = OrdersViewModel()
viewModel.markVerifyingForTests(orderNumber: "VERIFY001")
try await viewModel.verify(api: api, scenicId: 88, orderNumber: "VERIFY002")
XCTAssertTrue(api.verifiedOrderNumbers.isEmpty)
XCTAssertTrue(api.writeOffListCalls.isEmpty)
XCTAssertTrue(viewModel.isVerifying)
XCTAssertEqual(viewModel.currentVerifyingOrderNumber, "VERIFY001")
}
///
func testVerifyFailureClearsSubmittingStateWithoutRefreshingList() async throws {
let api = MockOrderService()
api.verifyError = APIError.httpStatus(500, "server error")
let viewModel = OrdersViewModel()
do {
try await viewModel.verify(api: api, scenicId: 88, orderNumber: "VERIFY001")
XCTFail("Expected verify to fail")
} catch APIError.httpStatus(let statusCode, _) {
XCTAssertEqual(statusCode, 500)
} catch {
XCTFail("Unexpected error: \(error)")
}
XCTAssertEqual(api.verifiedOrderNumbers, ["VERIFY001"])
XCTAssertTrue(api.writeOffListCalls.isEmpty)
XCTAssertFalse(viewModel.isVerifying)
XCTAssertNil(viewModel.currentVerifyingOrderNumber)
}
/// API
func testOrdersAPIUsesExpectedEndpointForRole() async throws {
let session = RecordingURLSession(data: try TestFixture.data(named: "order_v2_success"))
let api = OrdersAPI(client: APIClient(session: session))
_ = try await api.orderList(
scenicId: 88,
page: 1,
pageSize: 20,
orderStatus: 30,
userPhone: "13800000000",
startTime: "2026-05-01",
endTime: "2026-05-31",
isRefined: nil,
isScenicAdmin: true
)
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.url?.path, "/api/app/scenic-admin/order/list")
let query = queryItems(from: request)
XCTAssertEqual(query["scenic_id"], "88")
XCTAssertEqual(query["order_status"], "30")
XCTAssertEqual(query["user_phone"], "13800000000")
XCTAssertEqual(query["start_time"], "2026-05-01")
XCTAssertEqual(query["end_time"], "2026-05-31")
}
private static func date(_ text: String) -> Date {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: text)!
}
}
@MainActor
///
private final class MockOrderService: OrderServing {
struct OrderListCall: Equatable {
let scenicId: Int
let page: Int
let pageSize: Int
let orderStatus: Int?
let userPhone: String?
let startTime: String?
let endTime: String?
let isRefined: Int?
let isScenicAdmin: Bool
}
struct WriteOffListCall: Equatable {
let scenicId: Int
let storeId: Int?
let page: Int
let pageSize: Int
}
var storeResponses: [ListPayload<OrderEntity>] = []
var writeOffResponses: [ListPayload<WriteOffOrderItem>] = []
var detailResponses: [StoreOrderDetailResponse] = []
var verifyError: Error?
private(set) var orderListCalls: [OrderListCall] = []
private(set) var writeOffListCalls: [WriteOffListCall] = []
private(set) var detailCalls: [(storeId: Int, orderNumber: String)] = []
private(set) var verifiedOrderNumbers: [String] = []
///
func orderList(
scenicId: Int,
page: Int,
pageSize: Int,
orderStatus: Int?,
userPhone: String?,
startTime: String?,
endTime: String?,
isRefined: Int?,
isScenicAdmin: Bool
) async throws -> ListPayload<OrderEntity> {
orderListCalls.append(
OrderListCall(
scenicId: scenicId,
page: page,
pageSize: pageSize,
orderStatus: orderStatus,
userPhone: userPhone,
startTime: startTime,
endTime: endTime,
isRefined: isRefined,
isScenicAdmin: isScenicAdmin
)
)
return storeResponses.isEmpty ? ListPayload(total: 0, list: []) : storeResponses.removeFirst()
}
///
func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
writeOffListCalls.append(WriteOffListCall(scenicId: scenicId, storeId: storeId, page: page, pageSize: pageSize))
return writeOffResponses.isEmpty ? ListPayload(total: 0, list: []) : writeOffResponses.removeFirst()
}
///
func writeOff(orderNumber: String) async throws {
verifiedOrderNumbers.append(orderNumber)
if let verifyError {
throw verifyError
}
}
///
func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse {
detailCalls.append((storeId: storeId, orderNumber: orderNumber))
return detailResponses.isEmpty ? try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success") : detailResponses.removeFirst()
}
}
/// URLSession
private final class RecordingURLSession: URLSessionProtocol {
let data: Data
var statusCode: Int
private(set) var requests: [URLRequest] = []
init(data: Data, statusCode: Int = 200) {
self.data = data
self.statusCode = statusCode
}
/// HTTP
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
return (
data,
HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
)
}
}
private func queryItems(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) }
})
}