Files
2026-06-26 14:33:31 +08:00

185 lines
7.4 KiB
Swift

//
// ScheduleTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/24.
//
import XCTest
@testable import suixinkan_ios
@MainActor
/// API ViewModel
final class ScheduleTests: XCTestCase {
/// API path query/body
func testScheduleAPIUsesExpectedRequests() async throws {
let session = ScheduleRecordingURLSession(responses: [
Data(#"{"code":100000,"message":"ok","data":["2026-06-24"]}"#.utf8),
Data(#"{"code":100000,"message":"ok","data":[{"id":"1","name":"","start_time":"09:00","end_time":"10:00","schedule_date":"2026-06-24"}]}"#.utf8),
Data(#"{"code":100000,"message":"ok","data":{}}"#.utf8),
Data(#"{"code":100000,"message":"ok","data":{}}"#.utf8)
])
let api = ScheduleAPI(client: APIClient(session: session))
let days = try await api.monthScheduleDays(scenicId: 9, yearMonth: "2026-06")
let list = try await api.dayScheduleList(scenicId: 9, date: "2026-06-24")
try await api.addSchedule(AddScheduleRequest(scenicId: "9", name: "拍摄", remark: "备注", startTime: "09:00", endTime: "10:00", scheduleDate: "2026-06-24", orderNumber: "ORD001"))
try await api.deleteSchedule(id: 1)
XCTAssertEqual(days, ["2026-06-24"])
XCTAssertEqual(list.first?.id, 1)
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/yf-handset-app/photog/schedule/list-date",
"/api/yf-handset-app/photog/schedule/list",
"/api/yf-handset-app/photog/schedule/add",
"/api/yf-handset-app/photog/schedule/delete"
])
XCTAssertEqual(scheduleQueryItems(from: session.requests[0])["date"], "2026-06")
XCTAssertEqual(scheduleQueryItems(from: session.requests[1])["date"], "2026-06-24")
XCTAssertEqual((try scheduleBodyObject(from: session.requests[2]))["order_number"] as? String, "ORD001")
XCTAssertEqual((try scheduleBodyObject(from: session.requests[3]))["id"] as? Int, 1)
}
///
func testScheduleManagementWithoutScenicClearsState() async {
let api = FakeScheduleService()
let viewModel = ScheduleManagementViewModel()
await viewModel.reload(api: api, scenicId: nil)
XCTAssertTrue(viewModel.items.isEmpty)
XCTAssertTrue(viewModel.markedDays.isEmpty)
XCTAssertEqual(api.monthCalls.count, 0)
}
///
func testScheduleManagementReloadLoadsMonthAndDay() async {
let api = FakeScheduleService()
api.monthDays = ["2026-06-24"]
api.dayItems = [.fixture(id: 1)]
let viewModel = ScheduleManagementViewModel()
await viewModel.reload(api: api, scenicId: 9)
XCTAssertEqual(api.monthCalls.first?.scenicId, 9)
XCTAssertEqual(api.dayCalls.first?.scenicId, 9)
XCTAssertEqual(viewModel.markedDays, ["2026-06-24"])
XCTAssertEqual(viewModel.items.first?.id, 1)
}
/// 使
func testScheduleAddUsesSelectedOrderBeforeManualOrder() async {
let api = FakeScheduleService()
let viewModel = ScheduleAddViewModel()
viewModel.draft.name = "拍摄"
viewModel.draft.remark = ""
viewModel.draft.manualOrderNumber = "MANUAL"
viewModel.draft.selectedOrder = AvailableOrderResponse.fixture(orderNumber: "PICKED")
let start = fixedDate(hour: 9)
let end = fixedDate(hour: 10)
let success = await viewModel.submit(api: api, scenicId: 9, startDate: start, endDate: end)
XCTAssertTrue(success)
XCTAssertEqual(api.addedRequests.first?.orderNumber, "PICKED")
XCTAssertEqual(api.addedRequests.first?.remark, "拍摄")
}
///
func testScheduleAddRejectsInvalidTime() async {
let api = FakeScheduleService()
let viewModel = ScheduleAddViewModel()
viewModel.draft.name = "拍摄"
let success = await viewModel.submit(api: api, scenicId: 9, startDate: fixedDate(hour: 10), endDate: fixedDate(hour: 9))
XCTAssertFalse(success)
XCTAssertTrue(api.addedRequests.isEmpty)
XCTAssertEqual(viewModel.errorMessage, ScheduleValidationError.invalidTime.localizedDescription)
}
private func fixedDate(hour: Int) -> Date {
Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 24, hour: hour, minute: 0)) ?? Date()
}
}
private extension ScheduleItem {
///
static func fixture(id: Int) -> ScheduleItem {
let data = Data(#"{"id":\#(id),"name":"","start_time":"09:00","end_time":"10:00","schedule_date":"2026-06-24"}"#.utf8)
return try! JSONDecoder().decode(ScheduleItem.self, from: data)
}
}
private extension AvailableOrderResponse {
///
static func fixture(orderNumber: String) -> AvailableOrderResponse {
let data = Data(#"{"project_name":"","order_number":"\#(orderNumber)","order_status":"1","order_status_label":"","pay_time":"2026-06-24","user_phone":"13800000000"}"#.utf8)
return try! JSONDecoder().decode(AvailableOrderResponse.self, from: data)
}
}
/// API URLSession
private final class ScheduleRecordingURLSession: URLSessionProtocol {
private var responses: [Data]
private(set) var requests: [URLRequest] = []
/// Session
init(responses: [Data]) {
self.responses = responses
}
///
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
let data = responses.removeFirst()
return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
}
///
@MainActor
private final class FakeScheduleService: ScheduleServing {
var monthDays: [String] = []
var dayItems: [ScheduleItem] = []
var monthCalls: [(scenicId: Int, yearMonth: String)] = []
var dayCalls: [(scenicId: Int, date: String)] = []
var addedRequests: [AddScheduleRequest] = []
func monthScheduleDays(scenicId: Int, yearMonth: String) async throws -> [String] {
monthCalls.append((scenicId, yearMonth))
return monthDays
}
func dayScheduleList(scenicId: Int, date: String) async throws -> [ScheduleItem] {
dayCalls.append((scenicId, date))
return dayItems
}
func addSchedule(_ request: AddScheduleRequest) async throws {
addedRequests.append(request)
}
func deleteSchedule(id: Int) async throws {}
func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
[]
}
}
/// query
private func scheduleQueryItems(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) }
})
}
///
private func scheduleBodyObject(from request: URLRequest) throws -> [String: Any] {
let body = try XCTUnwrap(request.httpBody)
return try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
}