接入举报摄影师正式接口
This commit is contained in:
@ -6,7 +6,8 @@
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 举报摄影师模块测试,覆盖 Mock 数据、提交校验、列表筛选和补充证据。
|
||||
@MainActor
|
||||
/// 举报摄影师模块测试,覆盖 Mock 数据、接口类型、提交校验、列表筛选和补充证据。
|
||||
final class WildPhotographerReportTests: XCTestCase {
|
||||
func testSeedDataLoadsReportsAndRiskPoints() {
|
||||
let viewModel = WildPhotographerReportHomeViewModel()
|
||||
@ -21,7 +22,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
let beforeCount = viewModel.reports.count
|
||||
|
||||
let record = viewModel.submitReport(
|
||||
reportType: .suspectedWild,
|
||||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||||
description: "测试举报说明",
|
||||
location: "测试景区 · 测试点位",
|
||||
contact: "13800138000",
|
||||
@ -36,11 +37,154 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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.removeAttachment(viewModel.images[0])
|
||||
viewModel.removeAttachment(viewModel.videos[0])
|
||||
viewModel.updateDescription("有说明")
|
||||
viewModel.applyResolvedLocation("测试位置")
|
||||
|
||||
@ -53,6 +197,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
func testSubmitFailsWithoutDescription() {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
viewModel.addImage()
|
||||
viewModel.updateDescription(" ")
|
||||
viewModel.applyResolvedLocation("测试位置")
|
||||
|
||||
@ -65,6 +210,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
func testSubmitFailsWithoutConfirmedLocation() {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
viewModel.addImage()
|
||||
viewModel.updateDescription("测试说明")
|
||||
|
||||
viewModel.submitReport()
|
||||
@ -73,17 +219,77 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertNil(viewModel.submitResult)
|
||||
}
|
||||
|
||||
func testSubmitSuccessCreatesRecord() {
|
||||
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("测试景区 · 测试点")
|
||||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
|
||||
|
||||
viewModel.submitReport()
|
||||
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() {
|
||||
@ -103,6 +309,81 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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
|
||||
@ -120,3 +401,88 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user