Sync migrated iOS modules

This commit is contained in:
2026-06-22 15:35:12 +08:00
parent ace9c94359
commit 484566d27e
48 changed files with 4455 additions and 15 deletions

View File

@ -0,0 +1,306 @@
//
// 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)
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)
}
///
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, orderNumber: "VERIFY001")
XCTAssertEqual(api.verifiedOrderNumbers, ["VERIFY001"])
XCTAssertEqual(api.writeOffListCalls.count, 1)
XCTAssertEqual(api.writeOffListCalls[0].scenicId, 88)
XCTAssertEqual(api.writeOffListCalls[0].page, 1)
XCTAssertEqual(viewModel.writeOffOrders.count, 1)
XCTAssertEqual(viewModel.writeOffPage, 1)
XCTAssertFalse(viewModel.isVerifying)
XCTAssertNil(viewModel.currentVerifyingOrderNumber)
}
///
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 page: Int
let pageSize: Int
}
var storeResponses: [ListPayload<OrderEntity>] = []
var writeOffResponses: [ListPayload<WriteOffOrderItem>] = []
var verifyError: Error?
private(set) var orderListCalls: [OrderListCall] = []
private(set) var writeOffListCalls: [WriteOffListCall] = []
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, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
writeOffListCalls.append(WriteOffListCall(scenicId: scenicId, 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
}
}
}
/// 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) }
})
}