Files
suixinkan_ios_new/suixinkanTests/Schedule/ScheduleTests.swift
汉秋 fb8430889b Add Projects, Schedule, and Invite modules with home routing.
Migrate project management, schedule management, and photographer invitation flows from placeholders, including project image OSS upload and shared business models.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 15:57:15 +08:00

185 lines
7.4 KiB
Swift

//
// ScheduleTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/24.
//
import XCTest
@testable import suixinkan
@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])
}