Files
suixinkan_ios_new/suixinkanTests/PilotCertification/PilotCertificationTests.swift
汉秋 41dda3cc9b Add OperatingArea and PilotCertification modules with live push readiness.
Migrate operating area and pilot certification from home placeholders, extend Live with playback and push readiness flows, and add pilot certificate OSS upload.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 18:15:59 +08:00

278 lines
12 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.

//
// PilotCertificationTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/25.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class PilotCertificationTests: XCTestCase {
func testAPIsUseExpectedPathsAndBodies() async throws {
let session = PilotRecordingURLSession(responses: [
try TestFixture.data(named: "flyer_detail_success"),
try TestFixture.data(named: "empty_success"),
try TestFixture.data(named: "empty_success"),
try TestFixture.data(named: "empty_success")
])
let api = PilotCertificationAPI(client: APIClient(session: session))
let detail = try await api.flyerDetail()
XCTAssertEqual(detail.id, 801)
XCTAssertEqual(detail.certificateNo, "UAS-2026-001")
var request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.httpMethod, "GET")
XCTAssertEqual(request.url?.path, "/api/app/flyer/detail")
try await api.flyerSendCode(phone: "13800000000")
request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/app/flyer/send-code")
XCTAssertEqual(try pilotJSONBody(from: request)["phone"] as? String, "13800000000")
try await api.flyerApply(applyRequest())
request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/app/flyer/apply")
var json = try pilotJSONBody(from: request)
XCTAssertEqual(json["name"] as? String, "航拍摄影师")
XCTAssertEqual(json["certificate_no"] as? String, "UAS-2026-001")
XCTAssertEqual(json["drone_sn"] as? String, "SN20260523001")
try await api.flyerEdit(editRequest())
request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/app/flyer/edit")
json = try pilotJSONBody(from: request)
XCTAssertEqual(json["id"] as? Int, 801)
XCTAssertEqual(json["contact_phone"] as? String, "13800000001")
}
func testViewModelLoadSendCodeAndCountdown() async throws {
let api = PilotMock()
api.detailResponses = [try TestFixture.payload(FlyerDetailResponse.self, named: "flyer_detail_success")]
let realName = PilotRealNameMock(response: try TestFixture.payload(RealNameInfoResponse.self, named: "real_name_info_success"))
let viewModel = PilotCertificationViewModel()
await viewModel.load(api: api, realNameAPI: realName)
XCTAssertEqual(viewModel.name, "航拍摄影师")
XCTAssertEqual(viewModel.certType, .caac)
XCTAssertEqual(viewModel.droneSerialNo, "SN20260523001")
XCTAssertTrue(viewModel.isRealNameVerified)
XCTAssertEqual(viewModel.latestAuditLog?.operatorName, "审核员")
let codeViewModel = PilotCertificationViewModel()
fillValidForm(codeViewModel)
try await codeViewModel.sendCode(api: api)
XCTAssertEqual(api.sendCodePhones, ["13800000000"])
XCTAssertEqual(codeViewModel.countdown, 60)
codeViewModel.tickCountdown()
XCTAssertEqual(codeViewModel.countdown, 59)
api.sendCodeError = PilotTestError.sample
codeViewModel.phone = "13800000001"
while codeViewModel.countdown > 0 { codeViewModel.tickCountdown() }
await XCTAssertThrowsErrorAsync(try await codeViewModel.sendCode(api: api))
XCTAssertEqual(codeViewModel.countdown, 0)
}
func testValidationCoversRequiredFieldsPhoneImageAndDates() {
let viewModel = PilotCertificationViewModel()
XCTAssertEqual(viewModel.validationMessage, "请输入飞手昵称")
fillValidForm(viewModel)
viewModel.phone = "123"
XCTAssertEqual(viewModel.validationMessage, "请输入有效的手机号码")
fillValidForm(viewModel)
viewModel.certImageUrl = ""
XCTAssertEqual(viewModel.validationMessage, "请选择证件图片")
fillValidForm(viewModel)
viewModel.endDate = Date(timeInterval: -86_400, since: viewModel.startDate)
XCTAssertEqual(viewModel.validationMessage, "截至日期不能早于起始日期")
}
func testSubmitUploadsAndAppliesThenClearsPendingImage() async throws {
let api = PilotMock()
let uploader = PilotUploadMock()
let viewModel = PilotCertificationViewModel()
fillValidForm(viewModel)
viewModel.certImageUrl = ""
viewModel.prepareCertificateImage(data: Data([1, 2, 3]), fileName: "cert.jpg")
uploader.url = "https://cdn.example.com/pilot/cert.jpg"
try await viewModel.submit(api: api, uploader: uploader, scenicId: 88)
XCTAssertEqual(uploader.uploads.first?.fileName, "cert.jpg")
XCTAssertEqual(uploader.uploads.first?.scenicId, 88)
XCTAssertEqual(api.applyRequests.first?.certificateImage, "https://cdn.example.com/pilot/cert.jpg")
XCTAssertNil(viewModel.pendingCertificateImageData)
}
func testRejectedSubmitUsesEditAndFailureKeepsForm() async throws {
let api = PilotMock()
api.detailResponses = [
FlyerDetailResponse(id: 801, name: "旧资料", status: 3, certificateImage: "https://cdn.example.com/old.jpg")
]
let viewModel = PilotCertificationViewModel()
await viewModel.load(api: api, realNameAPI: PilotRealNameMock())
fillValidForm(viewModel)
try await viewModel.submit(api: api, uploader: PilotUploadMock(), scenicId: 88)
XCTAssertEqual(api.editRequests.first?.id, 801)
let failingUploader = PilotUploadMock()
failingUploader.error = PilotTestError.sample
viewModel.certImageUrl = ""
viewModel.prepareCertificateImage(data: Data([9]), fileName: "new.jpg")
await XCTAssertThrowsErrorAsync(try await viewModel.submit(api: api, uploader: failingUploader, scenicId: 88))
XCTAssertEqual(viewModel.pendingCertificateFileName, "new.jpg")
}
}
@MainActor
private final class PilotMock: PilotCertificationServing {
var detailResponses: [FlyerDetailResponse] = []
var sendCodePhones: [String] = []
var applyRequests: [FlyerApplyRequest] = []
var editRequests: [FlyerEditRequest] = []
var sendCodeError: Error?
func flyerDetail() async throws -> FlyerDetailResponse {
detailResponses.isEmpty ? FlyerDetailResponse() : detailResponses.removeFirst()
}
func flyerSendCode(phone: String) async throws {
if let sendCodeError { throw sendCodeError }
sendCodePhones.append(phone)
}
func flyerApply(_ request: FlyerApplyRequest) async throws {
applyRequests.append(request)
}
func flyerEdit(_ request: FlyerEditRequest) async throws {
editRequests.append(request)
}
}
private struct PilotRealNameMock: PilotRealNameServing {
var response = RealNameInfoResponse(realNameInfo: nil)
func realNameInfo() async throws -> RealNameInfoResponse {
response
}
}
@MainActor
private final class PilotUploadMock: OSSUploadServing {
var url = "https://cdn.example.com/pilot/cert.jpg"
var error: Error?
var uploads: [(fileName: String, scenicId: Int)] = []
func uploadPilotCertificateImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
if let error { throw error }
uploads.append((fileName, scenicId))
onProgress(100)
return url
}
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 uploadAliveAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadTaskFile(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 { "" }
}
private final class PilotRecordingURLSession: 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 ? try TestFixture.data(named: "empty_success") : responses.removeFirst()
return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
}
private enum PilotTestError: LocalizedError {
case sample
var errorDescription: String? { "测试错误" }
}
private func fillValidForm(_ viewModel: PilotCertificationViewModel) {
viewModel.name = "航拍摄影师"
viewModel.certType = .caac
viewModel.certNo = "UAS-2026-001"
viewModel.certImageUrl = "https://cdn.example.com/flyer/cert.jpg"
viewModel.startDate = Date(timeIntervalSince1970: 1_767_225_600)
viewModel.endDate = Date(timeIntervalSince1970: 1_830_384_000)
viewModel.droneModel = "DJI Mini"
viewModel.droneSerialNo = "SN20260523001"
viewModel.phone = "13800000000"
viewModel.verifyCode = "123456"
}
private func applyRequest() -> FlyerApplyRequest {
FlyerApplyRequest(
name: "航拍摄影师",
realnameStatus: 1,
certificateType: 2,
certificateNo: "UAS-2026-001",
certificateStartDate: "2026-01-01",
certificateEndDate: "2028-01-01",
certificateImage: "https://cdn.example.com/flyer/cert.jpg",
droneModel: "DJI Mini",
droneSn: "SN20260523001",
contactPhone: "13800000000",
code: "123456"
)
}
private func editRequest() -> FlyerEditRequest {
FlyerEditRequest(
id: 801,
name: "航拍摄影师",
realnameStatus: 1,
certificateType: 2,
certificateNo: "UAS-2026-001",
certificateStartDate: "2026-01-01",
certificateEndDate: "2028-01-01",
certificateImage: "https://cdn.example.com/flyer/cert.jpg",
droneModel: "DJI Mini",
droneSn: "SN20260523002",
contactPhone: "13800000001",
code: "654321"
)
}
private func pilotJSONBody(from request: URLRequest) throws -> [String: Any] {
let body = try XCTUnwrap(request.httpBody)
return try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
}
private func XCTAssertThrowsErrorAsync<T>(
_ expression: @autoclosure () async throws -> T,
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
_ = try await expression()
XCTFail("Expected async expression to throw", file: file, line: line)
} catch {
// Expected path.
}
}