Files
suixinkan_uikit/suixinkanTests/WildPhotographerReport/WildPhotographerReportTests.swift

869 lines
37 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 testReportDetailAPILoadsRemoteDetail() async throws {
let json = Data("""
{
"code": 100000,
"msg": "success",
"data": {
"id": 32,
"complaint_no": "JB20260708917",
"title": "",
"report_type": 1,
"report_type_text": "",
"desc": "",
"supplements": [],
"report_contents": [
{
"type": "initial",
"id": 0,
"desc": "",
"time": "2026-07-08 16:06:59",
"time_label": "16:06:59",
"evidences": [
{
"file_type": 1,
"file_type_text": "",
"file_url": "https://example.com/report.jpg",
"file_name": "report.jpg"
}
]
}
],
"scenic_id": 128,
"scenic_name": "伊犁那拉提景区-5A",
"store_user_id": 0,
"contact_phone": "13800138000",
"location_name": "伊犁那拉提景区-5A",
"location_address": "江苏省扬州市邗江区瘦西湖路",
"latitude": "32.4299428",
"longitude": "119.4403263",
"handle_status": 0,
"handle_status_text": "待处理",
"handle_remark": "",
"evidence_summary": { "image_count": 1, "video_count": 1 },
"evidences": [
{
"file_type": 1,
"file_type_text": "图片",
"file_url": "https://example.com/report.jpg",
"file_name": "report.jpg"
},
{
"file_type": 2,
"file_type_text": "视频",
"file_url": "https://example.com/report.mov",
"file_name": "report.mov"
}
],
"payment_screenshots": [
{
"file_type": 5,
"file_type_text": "支付截图",
"file_url": "https://example.com/payment.jpg",
"file_name": "payment.jpg"
}
],
"created_at": "2026-07-08 16:06:59",
"timeline": [
{ "time": "2026-07-08 16:06:59", "content": "摄影师提交举报" }
]
},
"time": "2026-07-08 16:31:50"
}
""".utf8)
let session = MockURLSession(responses: [json])
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
let response = try await api.reportDetail(id: 32)
let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/detail")
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
XCTAssertEqual(response.complaintNo, "JB20260708917")
XCTAssertEqual(response.evidences.count, 2)
XCTAssertEqual(response.paymentScreenshots.first?.fileName, "payment.jpg")
XCTAssertEqual(response.timeline.first?.content, "")
}
func testSupplementReportAPISendsRequestBody() async throws {
let json = Data("""
{
"code": 100000,
"msg": "success",
"data": null,
"time": "2026-07-08 17:31:50"
}
""".utf8)
let session = MockURLSession(responses: [json])
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
try await api.supplementReport(WildReportSupplementRequest(
id: 32,
desc: "",
evidences: [
WildReportEvidenceRequest(
fileURL: "https://example.com/supplement.jpg",
fileType: 1,
fileName: "supplement.jpg"
)
]
))
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/supplement")
let bodyData = try XCTUnwrap(request.httpBody)
let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
XCTAssertEqual(body["id"] as? Int, 32)
XCTAssertEqual(body["desc"] as? String, "补充说明")
let evidences = try XCTUnwrap(body["evidences"] as? [[String: Any]])
XCTAssertEqual(evidences.first?["file_url"] as? String, "https://example.com/supplement.jpg")
XCTAssertEqual(evidences.first?["file_type"] as? Int, 1)
XCTAssertEqual(evidences.first?["file_name"] as? String, "supplement.jpg")
}
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)
XCTAssertEqual(record.serverID, 32)
}
func testReportDetailMapsToRecordAndSupplementData() {
let detail = makeReportDetailData(handleStatus: 3, handleStatusText: "驳回")
let record = detail.toRecord()
XCTAssertEqual(record.serverID, 32)
XCTAssertEqual(record.id, "JB20260708917")
XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
XCTAssertEqual(record.status, .rejected)
XCTAssertEqual(record.location, "伊犁那拉提景区-5A · 江苏省扬州市邗江区瘦西湖路")
XCTAssertEqual(record.description, "正式详情说明")
XCTAssertEqual(record.phoneNumber, "13800138000")
XCTAssertEqual(record.imageCount, 1)
XCTAssertEqual(record.videoCount, 1)
XCTAssertEqual(detail.supplementRecords.first?.summaryText, "补充说明 · 1张图片")
}
func testReportStatusMapsRejected() {
XCTAssertEqual(WildReportStatus(handleStatus: 0), .pending)
XCTAssertEqual(WildReportStatus(handleStatus: 1), .processing)
XCTAssertEqual(WildReportStatus(handleStatus: 2), .processed)
XCTAssertEqual(WildReportStatus(handleStatus: 3), .rejected)
}
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() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
let uploader = MockWildReportAttachmentUploader()
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
XCTAssertEqual(viewModel.validationMessage, "请至少填写补充说明或上传一项证据。")
viewModel.updateText("补充说明")
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
viewModel.addVideos([try makeAttachment(kind: .video, fileName: "supplement.mov")])
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
XCTAssertTrue(viewModel.didSubmit)
XCTAssertEqual(uploader.uploadedFileNames, ["supplement.jpg", "supplement.mov"])
XCTAssertEqual(api.supplementRequests.first?.id, 32)
XCTAssertEqual(api.supplementRequests.first?.desc, "补充说明")
XCTAssertEqual(api.supplementRequests.first?.evidences?.map(\.fileType), [1, 2])
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.text, "补充说明")
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.imageCount, 1)
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.videoCount, 1)
}
func testSupplementEvidenceFailsWithoutServerID() async {
let homeViewModel = WildPhotographerReportHomeViewModel()
let record = WildReportRecord(
serverID: nil,
id: "JBLOCAL",
title: "摄影师举报",
reportType: WildReportType(value: 2, label: "疑似打野"),
status: .pending,
location: "测试景区",
submitTime: "2026-07-08 16:06:59",
description: "本地记录",
wechatID: "",
phoneNumber: "",
imageCount: 1,
videoCount: 0,
handlerInfo: nil
)
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
viewModel.updateText("补充说明")
await viewModel.submit(
api: MockWildPhotographerReportAPI(),
uploader: MockWildReportAttachmentUploader(),
scenicId: 128
)
XCTAssertEqual(viewModel.validationMessage, "当前举报暂未同步到服务器,无法补充证据")
XCTAssertFalse(viewModel.didSubmit)
}
func testSupplementEvidenceTextAndAttachmentLimits() throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
viewModel.updateText(String(repeating: "", count: 520))
XCTAssertEqual(viewModel.text.count, 500)
let images = try (0..<10).map { try makeAttachment(kind: .image, fileName: "image\($0).jpg") }
viewModel.addImages(images)
XCTAssertEqual(viewModel.images.count, 9)
viewModel.removeAttachment(viewModel.images[0])
let videos = try (0..<4).map { try makeAttachment(kind: .video, fileName: "video\($0).mov") }
viewModel.addVideos(videos)
XCTAssertEqual(viewModel.videos.count, 1)
}
func testSupplementEvidenceUploadFailureDoesNotSubmit() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
let uploader = MockWildReportAttachmentUploader()
uploader.error = URLError(.cannotWriteToFile)
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
XCTAssertFalse(viewModel.didSubmit)
XCTAssertTrue(api.supplementRequests.isEmpty)
XCTAssertEqual(viewModel.validationMessage, URLError(.cannotWriteToFile).localizedDescription)
}
func testSupplementEvidenceAPIErrorDoesNotSucceed() async throws {
let homeViewModel = WildPhotographerReportHomeViewModel()
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
let api = MockWildPhotographerReportAPI()
api.supplementError = URLError(.badServerResponse)
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
await viewModel.submit(
api: api,
uploader: MockWildReportAttachmentUploader(),
scenicId: 128
)
XCTAssertFalse(viewModel.didSubmit)
XCTAssertEqual(api.supplementRequests.count, 0)
XCTAssertEqual(viewModel.validationMessage, URLError(.badServerResponse).localizedDescription)
}
func testDetailViewModelLoadsRemoteDetailWhenServerIDExists() async {
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildPhotographerReportDetailViewModel(record: record)
let api = MockWildPhotographerReportAPI()
api.reportDetailResponses = [makeReportDetailData()]
await viewModel.loadDetailIfNeeded(api: api)
XCTAssertEqual(api.reportDetailRequests, [32])
XCTAssertEqual(viewModel.record.description, "正式详情说明")
XCTAssertEqual(viewModel.reporterMediaItems.count, 2)
XCTAssertEqual(viewModel.paymentScreenshotItems.count, 1)
XCTAssertEqual(viewModel.progressSteps.first?.title, "摄影师提交举报")
}
func testDetailViewModelFailureKeepsInitialRecord() async {
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
let viewModel = WildPhotographerReportDetailViewModel(record: record)
let api = MockWildPhotographerReportAPI()
api.reportDetailError = URLError(.badServerResponse)
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
await viewModel.loadDetailIfNeeded(api: api)
XCTAssertEqual(viewModel.record.description, "测试说明")
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
}
func testDetailViewModelWithoutServerIDSkipsRequest() async {
let record = WildReportRecord(
id: "JBLOCAL",
title: "摄影师举报",
reportType: WildReportType(value: 2, label: "疑似打野"),
status: .pending,
location: "测试景区",
submitTime: "2026-07-08 16:06:59",
description: "本地提交记录",
wechatID: "",
phoneNumber: "",
imageCount: 1,
videoCount: 0,
handlerInfo: nil
)
let viewModel = WildPhotographerReportDetailViewModel(record: record)
let api = MockWildPhotographerReportAPI()
await viewModel.loadDetailIfNeeded(api: api)
XCTAssertTrue(api.reportDetailRequests.isEmpty)
XCTAssertEqual(viewModel.record.description, "本地提交记录")
}
}
@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?
var reportDetailResponses: [WildReportDetailData] = []
var reportDetailRequests: [Int] = []
var reportDetailError: Error?
var supplementRequests: [WildReportSupplementRequest] = []
var supplementError: 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 supplementReport(_ request: WildReportSupplementRequest) async throws {
if let supplementError { throw supplementError }
supplementRequests.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()
}
func reportDetail(id: Int) async throws -> WildReportDetailData {
if let reportDetailError { throw reportDetailError }
reportDetailRequests.append(id)
guard !reportDetailResponses.isEmpty else { return WildReportDetailData(id: id) }
return reportDetailResponses.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(
id: Int = 32,
complaintNo: String,
title: String = "疑似打野摄影师举报",
reportType: Int = 2,
reportTypeText: String = "疑似打野",
handleStatus: Int,
handleStatusText: String = "待处理"
) -> WildReportListItem {
WildReportListItem(
id: id,
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"
)
}
private func makeReportDetailData(
handleStatus: Int = 0,
handleStatusText: String = "待处理"
) -> WildReportDetailData {
WildReportDetailData(
id: 32,
complaintNo: "JB20260708917",
title: "私下收款举报",
reportType: 1,
reportTypeText: "私下收款",
desc: "正式详情说明",
supplements: [
WildReportDetailContent(
type: "supplement",
id: 7,
desc: "补充说明",
time: "2026-07-08 17:06:59",
timeLabel: "17:06:59",
evidences: [
WildReportDetailEvidence(
fileType: 1,
fileTypeText: "图片",
fileURL: "https://example.com/supplement.jpg",
fileName: "supplement.jpg"
)
]
)
],
reportContents: [],
scenicId: 128,
scenicName: "伊犁那拉提景区-5A",
storeUserId: 0,
contactPhone: "13800138000",
locationName: "伊犁那拉提景区-5A",
locationAddress: "江苏省扬州市邗江区瘦西湖路",
latitude: "32.4299428",
longitude: "119.4403263",
handleStatus: handleStatus,
handleStatusText: handleStatusText,
handleRemark: "处理备注",
evidenceSummary: WildReportEvidenceSummary(imageCount: 1, videoCount: 1),
evidences: [
WildReportDetailEvidence(
fileType: 1,
fileTypeText: "图片",
fileURL: "https://example.com/report.jpg",
fileName: "report.jpg"
),
WildReportDetailEvidence(
fileType: 2,
fileTypeText: "视频",
fileURL: "https://example.com/report.mov",
fileName: "report.mov"
)
],
paymentScreenshots: [
WildReportDetailEvidence(
fileType: 5,
fileTypeText: "支付截图",
fileURL: "https://example.com/payment.jpg",
fileName: "payment.jpg"
)
],
timeline: [
WildReportDetailTimelineItem(time: "2026-07-08 16:06:59", content: "摄影师提交举报")
],
createdAt: "2026-07-08 16:06:59"
)
}