Files
suixinkan_uikit/suixinkanTests/WildPhotographerReport/WildPhotographerReportTests.swift

489 lines
21 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.

//
// WildPhotographerReportTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
/// Mock
final class WildPhotographerReportTests: XCTestCase {
func testSeedDataLoadsReportsAndRiskPoints() {
let viewModel = WildPhotographerReportHomeViewModel()
XCTAssertFalse(viewModel.reports.isEmpty)
XCTAssertFalse(viewModel.riskPoints.isEmpty)
XCTAssertEqual(viewModel.reports.count, WildPhotographerReportMockStore.seedReports.count)
}
func testSubmitReportGeneratesDynamicIDAndInsertsFirst() {
let viewModel = WildPhotographerReportHomeViewModel()
let beforeCount = viewModel.reports.count
let record = viewModel.submitReport(
reportType: WildReportType(value: 2, label: "疑似打野"),
description: "测试举报说明",
location: "测试景区 · 测试点位",
contact: "13800138000",
imageCount: 1,
videoCount: 0
)
XCTAssertEqual(viewModel.reports.count, beforeCount + 1)
XCTAssertEqual(viewModel.reports.first?.id, record.id)
XCTAssertTrue(record.id.hasPrefix("JB"))
XCTAssertEqual(record.status, .pending)
XCTAssertEqual(record.phoneNumber, "13800138000")
}
func testReportTypesAPILoadsRemoteOptions() async throws {
let json = Data("""
{
"code": 100000,
"msg": "success",
"data": {
"list": [
{ "value": 1, "label": "" },
{ "value": 2, "label": "" },
{ "value": 3, "label": "" },
{ "value": 4, "label": "" }
]
},
"time": "2026-07-08 14:21:21"
}
""".utf8)
let session = MockURLSession(responses: [json])
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
let response = try await api.reportTypes()
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/types")
XCTAssertEqual(response.list, [
WildReportType(value: 1, label: "私下收款"),
WildReportType(value: 2, label: "疑似打野"),
WildReportType(value: 3, label: "未戴工牌"),
WildReportType(value: 4, label: "其他")
])
}
func testReportListAPILoadsRemoteReports() async throws {
let json = Data("""
{
"code": 100000,
"msg": "success",
"data": {
"list": [
{
"id": 32,
"complaint_no": "JB20260708917",
"title": "",
"report_type": 1,
"report_type_text": "",
"desc": "",
"scenic_id": 128,
"scenic_name": "-5A",
"location_name": "-5A",
"latitude": "32.4299428",
"longitude": "119.4403263",
"handle_status": 0,
"handle_status_text": "",
"created_at": "2026-07-08 16:06:59"
}
],
"total": 1,
"page": 1,
"page_size": 10
},
"time": "2026-07-08 16:07:30"
}
""".utf8)
let session = MockURLSession(responses: [json])
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
let response = try await api.reportList(WildReportListRequest(scenicId: 128, handleStatus: 0, page: 1, pageSize: 10))
let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/list")
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "scenic_id", value: "128")) == true)
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "handle_status", value: "0")) == true)
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "page", value: "1")) == true)
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "page_size", value: "10")) == true)
XCTAssertEqual(response.total, 1)
XCTAssertEqual(response.page, 1)
XCTAssertEqual(response.pageSize, 10)
XCTAssertEqual(response.list.first?.complaintNo, "JB20260708917")
}
func testReportListItemMapsToRecord() {
let item = makeReportListItem(
complaintNo: "JB20260708917",
title: "私下收款举报",
reportType: 1,
reportTypeText: "私下收款",
handleStatus: 0,
handleStatusText: "待处理"
)
let record = item.toRecord()
XCTAssertEqual(record.id, "JB20260708917")
XCTAssertEqual(record.title, "私下收款举报")
XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
XCTAssertEqual(record.status, .pending)
XCTAssertEqual(record.location, "伊犁那拉提景区-5A")
XCTAssertEqual(record.description, "测试说明")
XCTAssertEqual(record.submitTime, "2026-07-08 16:06:59")
XCTAssertEqual(record.imageCount, 0)
XCTAssertEqual(record.videoCount, 0)
}
func testReportListAllFilterOmitsHandleStatus() {
let request = WildReportListRequest(scenicId: 128, handleStatus: nil, page: 1, pageSize: 10)
XCTAssertFalse(request.queryItems.contains { $0.name == "handle_status" })
XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "scenic_id", value: "128")))
XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "page", value: "1")))
XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "page_size", value: "10")))
}
func testSubmitViewModelLoadsReportTypesFromAPI() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.reportTypeResponse = WildReportTypeListResponse(list: [
WildReportType(value: 1, label: "私下收款"),
WildReportType(value: 2, label: "疑似打野"),
WildReportType(value: 4, label: "其他"),
])
await viewModel.loadReportTypes(api: api)
XCTAssertEqual(viewModel.reportTypes, [
WildReportType(value: 1, label: "私下收款"),
WildReportType(value: 2, label: "疑似打野"),
WildReportType(value: 4, label: "其他")
])
XCTAssertEqual(viewModel.selectedReportType, .defaultSelected)
}
func testSubmitViewModelFallsBackWhenReportTypesFail() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.error = URLError(.badServerResponse)
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
await viewModel.loadReportTypes(api: api)
XCTAssertEqual(viewModel.reportTypes, WildReportType.fallbackOptions)
XCTAssertEqual(viewModel.selectedReportType, .defaultSelected)
XCTAssertEqual(messages.last, "举报类型获取失败,已使用默认类型")
}
func testSubmitFailsWithoutEvidence() {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
viewModel.updateDescription("有说明")
viewModel.applyResolvedLocation("测试位置")
viewModel.submitReport()
XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
XCTAssertNil(viewModel.submitResult)
}
func testSubmitFailsWithoutDescription() {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
viewModel.addImage()
viewModel.updateDescription(" ")
viewModel.applyResolvedLocation("测试位置")
viewModel.submitReport()
XCTAssertEqual(viewModel.validationMessage, "请填写举报说明。")
XCTAssertNil(viewModel.submitResult)
}
func testSubmitFailsWithoutConfirmedLocation() {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
viewModel.addImage()
viewModel.updateDescription("测试说明")
viewModel.submitReport()
XCTAssertEqual(viewModel.validationMessage, "请先更新举报位置后再提交举报。")
XCTAssertNil(viewModel.submitResult)
}
func testSubmitSuccessCreatesRecord() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
let uploader = MockWildReportAttachmentUploader()
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
viewModel.updateDescription("测试说明")
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
XCTAssertNil(viewModel.validationMessage)
XCTAssertNotNil(viewModel.submitResult)
XCTAssertEqual(homeViewModel.reports.first?.id, viewModel.submitResult?.record.id)
XCTAssertEqual(uploader.uploadedFileNames, ["scene.jpg", "payment.jpg"])
XCTAssertEqual(api.submitRequests.first?.scenicId, 9)
XCTAssertEqual(api.submitRequests.first?.reportType, 2)
XCTAssertEqual(api.submitRequests.first?.lat, 30.12)
XCTAssertEqual(api.submitRequests.first?.lng, 120.34)
XCTAssertEqual(api.submitRequests.first?.evidences.count, 1)
XCTAssertEqual(api.submitRequests.first?.evidences.first?.fileType, 1)
XCTAssertEqual(api.submitRequests.first?.paymentScreenshots.count, 1)
XCTAssertNil(api.submitRequests.first?.storeUserId)
}
func testPaymentScreenshotsDoNotReplaceRequiredEvidence() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
viewModel.updateDescription("测试说明")
viewModel.applyResolvedLocation("测试景区 · 测试点")
await viewModel.submitReport(
api: MockWildPhotographerReportAPI(),
uploader: MockWildReportAttachmentUploader(),
scenicId: 9,
scenicName: "测试景区"
)
XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
}
func testPaymentScreenshotsLimitedToThree() throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
viewModel.addPaymentImages([
try makeAttachment(kind: .payment, fileName: "p1.jpg"),
try makeAttachment(kind: .payment, fileName: "p2.jpg"),
try makeAttachment(kind: .payment, fileName: "p3.jpg"),
try makeAttachment(kind: .payment, fileName: "p4.jpg")
])
XCTAssertEqual(viewModel.paymentImages.count, 3)
}
func testUploadFailureSkipsSubmitAPI() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
let uploader = MockWildReportAttachmentUploader()
uploader.error = OSSUploadError.emptyFile
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
viewModel.updateDescription("测试说明")
viewModel.applyResolvedLocation("测试景区 · 测试点")
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
XCTAssertEqual(api.submitRequests.count, 0)
XCTAssertEqual(viewModel.validationMessage, OSSUploadError.emptyFile.localizedDescription)
}
func testListFilterReturnsMatchingStatus() {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
viewModel.selectFilter(.pending)
XCTAssertTrue(viewModel.filteredReports.allSatisfy { $0.status == .pending })
viewModel.selectFilter(.processing)
XCTAssertTrue(viewModel.filteredReports.allSatisfy { $0.status == .processing })
viewModel.selectFilter(.processed)
XCTAssertTrue(viewModel.filteredReports.allSatisfy { $0.status == .processed })
viewModel.selectFilter(.all)
XCTAssertEqual(viewModel.filteredReports.count, homeViewModel.reports.count)
}
func testListInitialLoadUsesAllStatusAndFirstPage() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.reportListResponses = [
WildReportListResponse(
list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)],
total: 1,
page: 1,
pageSize: 10
)
]
await viewModel.loadInitial(api: api, scenicId: 128)
XCTAssertEqual(api.reportListRequests.first, WildReportListRequest(scenicId: 128, handleStatus: nil, page: 1, pageSize: 10))
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001"])
XCTAssertFalse(viewModel.hasMore)
}
func testListFilterReloadsWithMappedStatus() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.reportListResponses = [
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 1, page: 1, pageSize: 10),
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB002", handleStatus: 1)], total: 1, page: 1, pageSize: 10),
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB003", handleStatus: 2)], total: 1, page: 1, pageSize: 10),
]
await viewModel.selectFilter(.pending, api: api, scenicId: 128)
await viewModel.selectFilter(.processing, api: api, scenicId: 128)
await viewModel.selectFilter(.processed, api: api, scenicId: 128)
XCTAssertEqual(api.reportListRequests.map(\.handleStatus), [0, 1, 2] as [Int?])
XCTAssertEqual(api.reportListRequests.map(\.page), [1, 1, 1])
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB003"])
}
func testListLoadMoreAppendsReportsAndUpdatesHasMore() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.reportListResponses = [
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 2, page: 1, pageSize: 10),
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB002", handleStatus: 0)], total: 2, page: 2, pageSize: 10),
]
await viewModel.loadInitial(api: api, scenicId: 128)
XCTAssertTrue(viewModel.hasMore)
await viewModel.loadMoreIfNeeded(api: api, scenicId: 128)
XCTAssertEqual(api.reportListRequests.map(\.page), [1, 2])
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001", "JB002"])
XCTAssertFalse(viewModel.hasMore)
}
func testListLoadFailureKeepsExistingReports() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.reportListResponses = [
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 2, page: 1, pageSize: 10)
]
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
await viewModel.loadInitial(api: api, scenicId: 128)
api.reportListError = URLError(.badServerResponse)
await viewModel.loadMoreIfNeeded(api: api, scenicId: 128)
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001"])
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
}
func testSupplementEvidenceValidationAndSuccess() {
let homeViewModel = WildPhotographerReportHomeViewModel()
let reportID = homeViewModel.reports[0].id
let viewModel = WildReportSupplementEvidenceViewModel(reportID: reportID, homeViewModel: homeViewModel)
viewModel.submit()
XCTAssertEqual(viewModel.validationMessage, "请至少填写补充说明或上传一项证据。")
viewModel.updateText("补充说明")
viewModel.addImage()
viewModel.submit()
XCTAssertTrue(viewModel.didSubmit)
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: reportID).first?.text, "补充说明")
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: reportID).first?.imageCount, 1)
}
}
@MainActor
private final class MockWildPhotographerReportAPI: WildPhotographerReportServing {
var reportTypeResponse = WildReportTypeListResponse(list: [])
var error: Error?
var submitRequests: [WildReportSubmitRequest] = []
var reportListResponses: [WildReportListResponse] = []
var reportListRequests: [WildReportListRequest] = []
var reportListError: Error?
func reportTypes() async throws -> WildReportTypeListResponse {
if let error { throw error }
return reportTypeResponse
}
func submitReport(_ request: WildReportSubmitRequest) async throws {
if let error { throw error }
submitRequests.append(request)
}
func reportList(_ request: WildReportListRequest) async throws -> WildReportListResponse {
if let reportListError { throw reportListError }
reportListRequests.append(request)
guard !reportListResponses.isEmpty else {
return WildReportListResponse(list: [], total: 0, page: request.page, pageSize: request.pageSize)
}
return reportListResponses.removeFirst()
}
}
@MainActor
private final class MockWildReportAttachmentUploader: WildReportAttachmentUploading {
var uploadedFileNames: [String] = []
var error: Error?
func uploadWildReportAttachment(
data: Data,
fileName: String,
fileType: Int,
scenicId: Int,
onProgress: @escaping (Int) -> Void
) async throws -> String {
if let error { throw error }
onProgress(100)
uploadedFileNames.append(fileName)
return "https://oss.example.com/\(fileName)"
}
}
private func makeAttachment(kind: WildReportAttachmentKind, fileName: String) throws -> WildReportAttachment {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
try Data("mock".utf8).write(to: url)
return WildReportAttachment(
kind: kind,
title: fileName,
localFileURL: url,
fileName: fileName
)
}
private func makeReportListItem(
complaintNo: String,
title: String = "疑似打野摄影师举报",
reportType: Int = 2,
reportTypeText: String = "疑似打野",
handleStatus: Int,
handleStatusText: String = "待处理"
) -> WildReportListItem {
WildReportListItem(
id: Int(complaintNo.filter(\.isNumber)) ?? 1,
complaintNo: complaintNo,
title: title,
reportType: reportType,
reportTypeText: reportTypeText,
desc: "测试说明",
scenicId: 128,
scenicName: "伊犁那拉提景区-5A",
locationName: "伊犁那拉提景区-5A",
latitude: "32.4299428",
longitude: "119.4403263",
handleStatus: handleStatus,
handleStatusText: handleStatusText,
createdAt: "2026-07-08 16:06:59"
)
}