Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
314
suixinkan_iosTests/Assets/AssetsAPITests.swift
Normal file
314
suixinkan_iosTests/Assets/AssetsAPITests.swift
Normal file
@ -0,0 +1,314 @@
|
||||
//
|
||||
// AssetsAPITests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan_ios
|
||||
|
||||
@MainActor
|
||||
/// 资产 API 测试,覆盖云盘和素材接口请求以及宽松模型解码。
|
||||
final class AssetsAPITests: XCTestCase {
|
||||
/// 测试云盘列表接口使用正确 path 和 query。
|
||||
func testCloudFileListUsesExpectedPathAndQuery() async throws {
|
||||
let session = AssetsRecordingURLSession(data: Self.cloudListResponse)
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
|
||||
let payload = try await api.cloudFileList(parentFolderId: 9, name: " 照片 ", type: 2, orderBy: 1, page: 0, pageSize: 0)
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/cloud-driver/list")
|
||||
let query = assetsQueryItems(from: request)
|
||||
XCTAssertEqual(query["parent_folder_id"], "9")
|
||||
XCTAssertEqual(query["name"], " 照片 ")
|
||||
XCTAssertEqual(query["type"], "2")
|
||||
XCTAssertEqual(query["order_by"], "1")
|
||||
XCTAssertEqual(query["page"], "1")
|
||||
XCTAssertEqual(query["page_size"], "1")
|
||||
XCTAssertEqual(payload.total, 2)
|
||||
XCTAssertEqual(payload.list.dropFirst().first?.fileSize, 2048)
|
||||
}
|
||||
|
||||
/// 测试云盘写操作使用正确 path 和 body。
|
||||
func testCloudMutationRequestsUseExpectedBodies() async throws {
|
||||
let session = AssetsRecordingURLSession(data: Self.emptyResponse)
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
|
||||
try await api.cloudFolderCreate(CloudFolderCreateRequest(parentFolderId: 1, name: "新文件夹"))
|
||||
try await api.cloudFileUpload(CloudFileUploadRequest(parentFolderId: 1, fileUrl: "https://cdn/a.jpg", fileName: "a.jpg"))
|
||||
try await api.cloudFileDelete(CloudFileDeleteRequest(list: [CloudFileActionItem(id: 2, type: 99)]))
|
||||
try await api.cloudFileMove(CloudFileMoveRequest(targetFolderId: 3, list: [CloudFileActionItem(id: 2, type: 2)]))
|
||||
try await api.cloudFolderEdit(CloudFolderModifyRequest(id: 2, name: "改名"))
|
||||
try await api.cloudFileEdit(CloudFileModifyRequest(id: 4, fileName: "b.jpg"))
|
||||
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||
"/api/yf-handset-app/photog/cloud-driver/folder-create",
|
||||
"/api/yf-handset-app/photog/cloud-driver/file-upload",
|
||||
"/api/yf-handset-app/photog/cloud-driver/delete",
|
||||
"/api/yf-handset-app/photog/cloud-driver/move",
|
||||
"/api/yf-handset-app/photog/cloud-driver/folder-edit",
|
||||
"/api/yf-handset-app/photog/cloud-driver/file-edit"
|
||||
])
|
||||
let uploadBody = try bodyObject(from: session.requests[1])
|
||||
XCTAssertEqual(uploadBody["parent_folder_id"] as? Int, 1)
|
||||
XCTAssertEqual(uploadBody["file_url"] as? String, "https://cdn/a.jpg")
|
||||
let moveBody = try bodyObject(from: session.requests[3])
|
||||
XCTAssertEqual(moveBody["target_folder_id"] as? Int, 3)
|
||||
}
|
||||
|
||||
/// 测试云盘上传权限接口兼容字符串布尔值。
|
||||
func testCloudUploadPermissionDecodesLossyFields() async throws {
|
||||
let session = AssetsRecordingURLSession(data: Self.permissionResponse)
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
|
||||
let permission = try await api.checkCloudUploadPermission()
|
||||
|
||||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/cloud-driver/check-upload-permission")
|
||||
XCTAssertFalse(permission.canUpload)
|
||||
XCTAssertEqual(permission.reason, "空间不足")
|
||||
}
|
||||
|
||||
/// 测试素材列表和详情接口使用正确 path 并宽松解码字段。
|
||||
func testMediaListAndDetailDecodeLossyFields() async throws {
|
||||
let session = AssetsRecordingURLSession(responses: [Self.mediaListResponse, Self.mediaDetailResponse])
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
|
||||
let list = try await api.mediaAlbumList(kind: .material, keyword: "航拍", status: 1, page: 0, pageSize: 0)
|
||||
let detail = try await api.mediaAlbumDetail(id: 7, kind: .material)
|
||||
|
||||
XCTAssertEqual(session.requests.first?.url?.path, "/api/app/media-album/list")
|
||||
let query = assetsQueryItems(from: try XCTUnwrap(session.requests.first))
|
||||
XCTAssertEqual(query["type"], "1")
|
||||
XCTAssertEqual(query["status"], "1")
|
||||
XCTAssertEqual(query["page"], "1")
|
||||
XCTAssertEqual(query["page_size"], "1")
|
||||
XCTAssertEqual(list.order?.totalNum, 12)
|
||||
XCTAssertEqual(list.list.first?.likesCount, 3)
|
||||
XCTAssertEqual(session.requests.last?.url?.path, "/api/app/media-album/detail")
|
||||
XCTAssertEqual(assetsQueryItems(from: try XCTUnwrap(session.requests.last))["type"], "1")
|
||||
XCTAssertEqual(detail.mediaList.first?.size, 4096)
|
||||
XCTAssertTrue(detail.mediaList.first?.isVideo == true)
|
||||
}
|
||||
|
||||
/// 测试素材写操作 path 和请求体。
|
||||
func testMediaMutationRequestsUseExpectedBodies() async throws {
|
||||
let session = AssetsRecordingURLSession(data: Self.emptyResponse)
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
let upload = MediaAlbumUploadRequest(
|
||||
name: "素材",
|
||||
type: 1,
|
||||
mediaType: 2,
|
||||
mediaList: [MediaAlbumUploadItem(originalName: "a.jpg", ossUrl: "https://cdn/a.jpg", size: 10, fileWidthSize: MediaAlbumFileSize(width: 100, height: 80))],
|
||||
coverUrl: "https://cdn/c.jpg",
|
||||
coverSize: MediaAlbumFileSize(width: 100, height: 80),
|
||||
scenicSpotId: 6,
|
||||
description: "描述",
|
||||
materialTag: "航拍",
|
||||
projectId: 0
|
||||
)
|
||||
|
||||
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 1, type: 1, listingStatus: 1))
|
||||
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 1, type: 1))
|
||||
try await api.mediaAlbumAddTag(MediaAlbumAddTagRequest(name: "航拍"))
|
||||
try await api.mediaAlbumUpload(upload)
|
||||
try await api.mediaAlbumEdit(MediaAlbumEditRequest(id: 2, name: "素材", type: 1, mediaType: 2, mediaList: upload.mediaList, coverUrl: upload.coverUrl, coverSize: upload.coverSize, scenicSpotId: 6, description: "描述", materialTag: "航拍"))
|
||||
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||
"/api/app/media-album/operation",
|
||||
"/api/app/media-album/delete",
|
||||
"/api/app/media-album/add-tag",
|
||||
"/api/app/media-album/upload",
|
||||
"/api/app/media-album/edit"
|
||||
])
|
||||
let body = try bodyObject(from: session.requests[3])
|
||||
XCTAssertEqual(body["scenic_spot_id"] as? Int, 6)
|
||||
XCTAssertEqual((body["media_list"] as? [[String: Any]])?.first?["oss_url"] as? String, "https://cdn/a.jpg")
|
||||
}
|
||||
|
||||
/// 测试样片请求携带 type=2,项目列表接口和项目字段宽松解码正确。
|
||||
func testSampleRequestsAndProjectListUseExpectedTypeAndQuery() async throws {
|
||||
let session = AssetsRecordingURLSession(responses: [
|
||||
Self.mediaListResponse,
|
||||
Self.mediaDetailResponse,
|
||||
Self.emptyResponse,
|
||||
Self.emptyResponse,
|
||||
Self.projectListResponse,
|
||||
Self.emptyResponse
|
||||
])
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
let upload = MediaAlbumUploadRequest(
|
||||
name: "亲子样片",
|
||||
type: 2,
|
||||
mediaType: 2,
|
||||
mediaList: [MediaAlbumUploadItem(originalName: "sample.jpg", ossUrl: "https://cdn/sample.jpg", size: 10, fileWidthSize: MediaAlbumFileSize(width: 100, height: 80))],
|
||||
coverUrl: "https://cdn/cover.jpg",
|
||||
coverSize: MediaAlbumFileSize(width: 100, height: 80),
|
||||
scenicSpotId: 6,
|
||||
description: "描述",
|
||||
materialTag: "",
|
||||
projectId: 88
|
||||
)
|
||||
|
||||
_ = try await api.mediaAlbumList(kind: .sample, keyword: "样片", status: nil, page: 1, pageSize: 10)
|
||||
_ = try await api.mediaAlbumDetail(id: 7, kind: .sample)
|
||||
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 7, type: 2, listingStatus: 1))
|
||||
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 7, type: 2))
|
||||
let projects = try await api.projectList(scenicId: 9, name: "亲子", page: 0, pageSize: 0)
|
||||
try await api.mediaAlbumUpload(upload)
|
||||
|
||||
XCTAssertEqual(assetsQueryItems(from: session.requests[0])["type"], "2")
|
||||
XCTAssertEqual(assetsQueryItems(from: session.requests[1])["type"], "2")
|
||||
XCTAssertEqual((try bodyObject(from: session.requests[2]))["type"] as? Int, 2)
|
||||
XCTAssertEqual((try bodyObject(from: session.requests[3]))["type"] as? Int, 2)
|
||||
XCTAssertEqual(session.requests[4].url?.path, "/api/yf-handset-app/photog/project/list")
|
||||
let projectQuery = assetsQueryItems(from: session.requests[4])
|
||||
XCTAssertEqual(projectQuery["scenic_id"], "9")
|
||||
XCTAssertEqual(projectQuery["name"], "亲子")
|
||||
XCTAssertEqual(projectQuery["page"], "1")
|
||||
XCTAssertEqual(projectQuery["page_size"], "1")
|
||||
XCTAssertEqual(projects.list.first?.id, 88)
|
||||
XCTAssertEqual(projects.list.first?.name, "亲子旅拍")
|
||||
XCTAssertEqual((try bodyObject(from: session.requests[5]))["project_id"] as? Int, 88)
|
||||
}
|
||||
|
||||
/// 测试相册接口使用旧工程路径、query 和请求体。
|
||||
func testAlbumRequestsUseExpectedPathQueryAndBodies() async throws {
|
||||
let session = AssetsRecordingURLSession(responses: [
|
||||
Self.albumFolderListResponse,
|
||||
Self.albumFolderInfoResponse,
|
||||
Self.albumFileListResponse,
|
||||
Self.emptyResponse,
|
||||
Self.emptyResponse,
|
||||
Self.emptyResponse,
|
||||
Self.emptyResponse
|
||||
])
|
||||
let api = AssetsAPI(client: APIClient(session: session))
|
||||
|
||||
let folders = try await api.albumFolderList(
|
||||
scenicId: 9,
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
name: "旅拍",
|
||||
startTime: "2026-06-01",
|
||||
endTime: "2026-06-24"
|
||||
)
|
||||
let info = try await api.albumFolderInfo(id: 3)
|
||||
let files = try await api.albumFileList(scenicId: 9, folderId: 3, fileType: 2, page: 0, pageSize: 0)
|
||||
try await api.addAlbumFolder(scenicId: 9, name: "新相册", remark: "备注")
|
||||
try await api.editAlbumFolder(folderId: 3, coverFileId: 11, name: "改名", remark: nil)
|
||||
try await api.deleteAlbumFiles(folderId: 3, idList: [11, 12])
|
||||
try await api.albumFileUploadURL(scenicId: 9, fileURL: "https://cdn/a.jpg", folderId: 3)
|
||||
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||
"/api/yf-handset-app/photog/album/folder-list",
|
||||
"/api/yf-handset-app/photog/album/folder-info",
|
||||
"/api/yf-handset-app/photog/album/file-list",
|
||||
"/api/yf-handset-app/photog/album/folder-add",
|
||||
"/api/yf-handset-app/photog/album/folder-edit",
|
||||
"/api/yf-handset-app/photog/album/file-delete",
|
||||
"/api/yf-handset-app/photog/album/file-upload-url"
|
||||
])
|
||||
let folderQuery = assetsQueryItems(from: session.requests[0])
|
||||
XCTAssertEqual(folderQuery["scenic_id"], "9")
|
||||
XCTAssertEqual(folderQuery["page"], "1")
|
||||
XCTAssertEqual(folderQuery["page_size"], "1")
|
||||
XCTAssertEqual(folderQuery["cloud_folder_type"], "1")
|
||||
XCTAssertEqual(folderQuery["name"], "旅拍")
|
||||
XCTAssertEqual(folderQuery["start_time"], "2026-06-01")
|
||||
XCTAssertEqual(folderQuery["end_time"], "2026-06-24")
|
||||
XCTAssertEqual(folders.list.first?.countImage, 4)
|
||||
XCTAssertEqual(info.coverFile?.fileSize, 2048)
|
||||
XCTAssertTrue(files.list.first?.isImage == true)
|
||||
|
||||
let addBody = try bodyObject(from: session.requests[3])
|
||||
XCTAssertEqual(addBody["scenic_id"] as? String, "9")
|
||||
XCTAssertEqual(addBody["cloud_folder_type"] as? Int, 1)
|
||||
let editBody = try bodyObject(from: session.requests[4])
|
||||
XCTAssertEqual(editBody["cover_file_id"] as? Int, 11)
|
||||
let deleteBody = try bodyObject(from: session.requests[5])
|
||||
XCTAssertEqual(deleteBody["folder_id"] as? Int, 3)
|
||||
XCTAssertEqual(deleteBody["id_list"] as? [Int], [11, 12])
|
||||
let uploadQuery = assetsQueryItems(from: session.requests[6])
|
||||
XCTAssertEqual(uploadQuery["file_url"], "https://cdn/a.jpg")
|
||||
}
|
||||
|
||||
fileprivate static let emptyResponse = #"{"code":100000,"msg":"success","data":{}}"#.data(using: .utf8)!
|
||||
fileprivate static let permissionResponse = #"{"code":100000,"msg":"success","data":{"can_upload":"0","reason":"空间不足"}}"#.data(using: .utf8)!
|
||||
fileprivate static let cloudListResponse = """
|
||||
{"code":100000,"msg":"success","data":{"total":"2","list":[
|
||||
{"id":"1","parent_folder_id":0,"file_url":"","cover_url":"","updated_at":"2026","name":"文件夹","created_at":"2026","child_num":"3","type":"99","file_size":"0"},
|
||||
{"id":2,"parent_folder_id":"0","file_url":"https://cdn/a.jpg","cover_url":"","updated_at":"2026","name":"a.jpg","created_at":"2026","child_num":0,"type":2,"file_size":"2048"}
|
||||
]}}
|
||||
""".data(using: .utf8)!
|
||||
fileprivate static let mediaListResponse = """
|
||||
{"code":100000,"msg":"success","data":{"total":"1","order":{"total_num":"12","avg_order_amount":"8.5","refund_total":1,"avg_change":"0.2"},"list":[
|
||||
{"id":"7","name":"航拍","cover_url":"https://cdn/c.jpg","created_at":"2026","status":"1","download_count":"2","likes_count":"3","collect_count":"4","share_count":"5","scenic_spot_name":"东门","project_name":"项目","listing_status":"1"}
|
||||
]}}
|
||||
""".data(using: .utf8)!
|
||||
fileprivate static let mediaDetailResponse = """
|
||||
{"code":100000,"msg":"success","data":{"id":"7","name":"航拍","cover_url":"https://cdn/c.jpg","description":"描述","status":"1","listing_status":"1","likes_count":"3","download_count":"2","collect_count":"4","share_count":"5","uploader_name":"摄影师","created_at":"2026","scenic_name":"景区","project_name":"项目","media_list":[
|
||||
{"id":"9","original_name":"v.mp4","oss_url":"https://cdn/v.mp4","thumbnail_url":"","type":"1","size":"4096","show_url":"https://cdn/v.mp4"}
|
||||
]}}
|
||||
""".data(using: .utf8)!
|
||||
fileprivate static let projectListResponse = """
|
||||
{"code":100000,"msg":"success","data":{"total":"1","list":[
|
||||
{"id":"88","type":"19","type_name":"多点旅拍","status":"1","status_name":"上架","name":"亲子旅拍","cover_project":"https://cdn/p.jpg","cover_video":"","price":"299","ot_price":"399","price_deposit":"99"}
|
||||
]}}
|
||||
""".data(using: .utf8)!
|
||||
fileprivate static let albumFolderListResponse = """
|
||||
{"code":100000,"msg":"success","data":{"total":"1","list":[
|
||||
{"id":"3","name":"旅拍","cover_file_id":"11","count_video":"2","count_image":"4","cover":{"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""},"created_at":"2026","remark":"备注"}
|
||||
]}}
|
||||
""".data(using: .utf8)!
|
||||
fileprivate static let albumFolderInfoResponse = """
|
||||
{"code":100000,"msg":"success","data":{"id":"3","name":"旅拍","cover_file_id":"11","count_video":"2","count_image":"4","cover":{"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""},"created_at":"2026","remark":"备注"}}
|
||||
""".data(using: .utf8)!
|
||||
fileprivate static let albumFileListResponse = """
|
||||
{"code":100000,"msg":"success","data":{"total":"1","list":[
|
||||
{"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""}
|
||||
]}}
|
||||
""".data(using: .utf8)!
|
||||
}
|
||||
|
||||
/// 资产 API 测试用 URLSession,记录请求并返回固定响应。
|
||||
private final class AssetsRecordingURLSession: URLSessionProtocol {
|
||||
private var responses: [Data]
|
||||
private(set) var requests: [URLRequest] = []
|
||||
|
||||
/// 初始化单一响应。
|
||||
init(data: Data) {
|
||||
responses = [data]
|
||||
}
|
||||
|
||||
/// 初始化顺序响应。
|
||||
init(responses: [Data]) {
|
||||
self.responses = responses
|
||||
}
|
||||
|
||||
/// 记录请求并返回下一份响应。
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
requests.append(request)
|
||||
let data = responses.isEmpty ? AssetsAPITests.emptyResponse : responses.removeFirst()
|
||||
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
return (data, response)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从请求中提取 query 字典。
|
||||
private func assetsQueryItems(from request: URLRequest) -> [String: String] {
|
||||
guard
|
||||
let url = request.url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
else {
|
||||
return [:]
|
||||
}
|
||||
return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") })
|
||||
}
|
||||
|
||||
/// 将请求体解析成字典。
|
||||
private func bodyObject(from request: URLRequest) throws -> [String: Any] {
|
||||
let data = try XCTUnwrap(request.httpBody)
|
||||
return try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
}
|
||||
675
suixinkan_iosTests/Assets/AssetsViewModelTests.swift
Normal file
675
suixinkan_iosTests/Assets/AssetsViewModelTests.swift
Normal file
@ -0,0 +1,675 @@
|
||||
//
|
||||
// AssetsViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan_ios
|
||||
|
||||
@MainActor
|
||||
/// 资产 ViewModel 测试,覆盖云盘和素材管理核心业务逻辑。
|
||||
final class AssetsViewModelTests: XCTestCase {
|
||||
/// 测试云盘无上传权限时不会继续上传 OSS 或写入云盘。
|
||||
func testCloudUploadStopsWhenPermissionDenied() async {
|
||||
let api = MockAssetsService()
|
||||
api.uploadPermission = false
|
||||
api.uploadReason = "空间不足"
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = CloudStorageViewModel()
|
||||
|
||||
let success = await viewModel.upload(
|
||||
localFiles: [CloudLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)],
|
||||
scenicId: 9,
|
||||
api: api,
|
||||
uploadService: uploader,
|
||||
transferStore: CloudTransferStore()
|
||||
)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(viewModel.errorMessage, "空间不足")
|
||||
XCTAssertEqual(uploader.uploadedFiles.count, 0)
|
||||
XCTAssertEqual(api.cloudUploadRequests.count, 0)
|
||||
}
|
||||
|
||||
/// 测试云盘上传会先上传 OSS,再登记云盘文件并刷新列表。
|
||||
func testCloudUploadUploadsOSSBeforeCloudRecord() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
let transferStore = CloudTransferStore()
|
||||
let viewModel = CloudStorageViewModel()
|
||||
|
||||
let success = await viewModel.upload(
|
||||
localFiles: [CloudLocalUploadFile(data: Data([1, 2]), fileName: "a.jpg", fileType: 2)],
|
||||
scenicId: 9,
|
||||
api: api,
|
||||
uploadService: uploader,
|
||||
transferStore: transferStore
|
||||
)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["a.jpg"])
|
||||
XCTAssertEqual(api.cloudUploadRequests.first?.fileUrl, "https://cdn.example.com/a.jpg")
|
||||
XCTAssertEqual(transferStore.records.first?.status, .success)
|
||||
}
|
||||
|
||||
/// 测试云盘分页、进入文件夹和返回文件夹。
|
||||
func testCloudPaginationAndFolderNavigation() async {
|
||||
let folder = CloudDriveFile(id: 2, parentFolderId: 0, fileUrl: "", name: "子目录", type: 99)
|
||||
let api = MockAssetsService()
|
||||
api.cloudPages = [
|
||||
ListPayload(total: 2, list: [folder]),
|
||||
ListPayload(total: 2, list: [CloudDriveFile(id: 3, parentFolderId: 0, fileUrl: "https://cdn/a.jpg", name: "a.jpg", type: 2)])
|
||||
]
|
||||
let viewModel = CloudStorageViewModel()
|
||||
|
||||
await viewModel.reload(api: api)
|
||||
await viewModel.loadMore(api: api)
|
||||
await viewModel.enterFolder(folder, api: api)
|
||||
await viewModel.popToFolder(at: 0, api: api)
|
||||
|
||||
XCTAssertEqual(api.cloudListRequests.first?.parentFolderId, 0)
|
||||
XCTAssertEqual(api.cloudListRequests[1].page, 2)
|
||||
XCTAssertEqual(api.cloudListRequests[2].parentFolderId, 2)
|
||||
XCTAssertEqual(viewModel.currentFolderId, 0)
|
||||
}
|
||||
|
||||
/// 测试素材列表筛选和分页请求参数。
|
||||
func testMediaListFilterAndPagination() async {
|
||||
let api = MockAssetsService()
|
||||
api.mediaListResponses = [
|
||||
MediaLibraryListResponse.fixture(total: 2, ids: [1]),
|
||||
MediaLibraryListResponse.fixture(total: 2, ids: [2])
|
||||
]
|
||||
let viewModel = MediaLibraryViewModel(kind: .material)
|
||||
viewModel.keyword = " 航拍 "
|
||||
viewModel.selectedAuditFilter = .approved
|
||||
|
||||
await viewModel.reload(api: api)
|
||||
await viewModel.loadMore(api: api)
|
||||
|
||||
XCTAssertEqual(api.mediaListRequests.first?.keyword, "航拍")
|
||||
XCTAssertEqual(api.mediaListRequests.first?.status, 1)
|
||||
XCTAssertEqual(api.mediaListRequests.last?.page, 2)
|
||||
XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
|
||||
}
|
||||
|
||||
/// 测试样片列表请求使用样片类型。
|
||||
func testSampleMediaListUsesSampleKind() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = MediaLibraryViewModel(kind: .sample)
|
||||
|
||||
await viewModel.reload(api: api)
|
||||
|
||||
XCTAssertEqual(api.mediaListRequests.first?.kind, .sample)
|
||||
}
|
||||
|
||||
/// 测试审核未通过素材禁止上下架。
|
||||
func testRejectedMediaCannotToggleListing() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = MediaLibraryViewModel()
|
||||
let item = MediaLibraryItem.fixture(id: 1, status: 2, listingStatus: 0)
|
||||
|
||||
let success = await viewModel.toggleListing(item, api: api)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(api.mediaOperationRequests.count, 0)
|
||||
XCTAssertEqual(viewModel.errorMessage, "审核通过后才能上下架")
|
||||
}
|
||||
|
||||
/// 测试样片上下架会携带样片类型。
|
||||
func testSampleToggleListingUsesSampleType() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = MediaLibraryViewModel(kind: .sample)
|
||||
let item = MediaLibraryItem.fixture(id: 1, status: 1, listingStatus: 0)
|
||||
|
||||
let success = await viewModel.toggleListing(item, api: api)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(api.mediaOperationRequests.first?.type, 2)
|
||||
}
|
||||
|
||||
/// 测试上传素材会先上传封面和媒体文件,再提交最终 OSS URL。
|
||||
func testMediaUploadUploadsFilesBeforeSubmit() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = MediaLibraryEditorViewModel()
|
||||
viewModel.name = "航拍素材"
|
||||
viewModel.selectedSpotId = 6
|
||||
viewModel.tagsText = "航拍,夜景"
|
||||
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
|
||||
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
|
||||
|
||||
let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["cover.jpg", "a.jpg"])
|
||||
XCTAssertEqual(api.mediaUploadRequests.first?.coverUrl, "https://cdn.example.com/cover.jpg")
|
||||
XCTAssertEqual(api.mediaUploadRequests.first?.mediaList.first?.ossUrl, "https://cdn.example.com/a.jpg")
|
||||
XCTAssertEqual(api.mediaUploadRequests.first?.materialTag, "航拍,夜景")
|
||||
}
|
||||
|
||||
/// 测试素材上传失败时不会提交素材接口。
|
||||
func testMediaUploadFailureDoesNotSubmit() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
uploader.shouldFail = true
|
||||
let viewModel = MediaLibraryEditorViewModel()
|
||||
viewModel.name = "航拍素材"
|
||||
viewModel.selectedSpotId = 6
|
||||
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
|
||||
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
|
||||
|
||||
let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(api.mediaUploadRequests.count, 0)
|
||||
}
|
||||
|
||||
/// 测试样片上传缺少关联项目时不提交。
|
||||
func testSampleUploadRequiresProject() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = MediaLibraryEditorViewModel()
|
||||
viewModel.name = "亲子样片"
|
||||
viewModel.selectedSpotId = 6
|
||||
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
|
||||
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
|
||||
|
||||
let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请选择关联项目")
|
||||
XCTAssertEqual(api.mediaUploadRequests.count, 0)
|
||||
}
|
||||
|
||||
/// 测试样片上传会先上传文件,再提交样片类型和关联项目。
|
||||
func testSampleUploadUploadsFilesAndSubmitsProject() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = MediaLibraryEditorViewModel()
|
||||
viewModel.name = "亲子样片"
|
||||
viewModel.selectedSpotId = 6
|
||||
viewModel.selectedProjectId = 88
|
||||
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
|
||||
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
|
||||
|
||||
let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["cover.jpg", "a.jpg"])
|
||||
XCTAssertEqual(api.mediaUploadRequests.first?.type, 2)
|
||||
XCTAssertEqual(api.mediaUploadRequests.first?.projectId, 88)
|
||||
}
|
||||
|
||||
/// 测试重复提交样片会被忽略。
|
||||
func testSampleDuplicateSubmitIsIgnored() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = MediaLibraryEditorViewModel()
|
||||
viewModel.name = "亲子样片"
|
||||
viewModel.selectedSpotId = 6
|
||||
viewModel.selectedProjectId = 88
|
||||
viewModel.isSubmitting = true
|
||||
viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
|
||||
viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])
|
||||
|
||||
let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(api.mediaUploadRequests.count, 0)
|
||||
XCTAssertEqual(uploader.uploadedFiles.count, 0)
|
||||
}
|
||||
|
||||
/// 测试相册列表无景区时清空数据且不请求接口。
|
||||
func testAlbumListClearsWithoutScenic() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = AlbumListViewModel()
|
||||
|
||||
await viewModel.reload(api: api, scenicId: nil)
|
||||
|
||||
XCTAssertTrue(viewModel.folders.isEmpty)
|
||||
XCTAssertEqual(api.albumFolderListRequests.count, 0)
|
||||
XCTAssertEqual(viewModel.errorMessage, "缺少当前景区,无法加载相册")
|
||||
}
|
||||
|
||||
/// 测试相册列表搜索、日期筛选和分页参数。
|
||||
func testAlbumListFilterAndPagination() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFolderPages = [
|
||||
ListPayload(total: 2, list: [AlbumFolderItem.fixture(id: 1, name: "旅拍")]),
|
||||
ListPayload(total: 2, list: [AlbumFolderItem.fixture(id: 2, name: "亲子")])
|
||||
]
|
||||
let viewModel = AlbumListViewModel()
|
||||
viewModel.searchText = " 旅拍 "
|
||||
viewModel.startTime = "2026-06-01"
|
||||
viewModel.endTime = "2026-06-24"
|
||||
|
||||
await viewModel.reload(api: api, scenicId: 9)
|
||||
await viewModel.loadMore(api: api, scenicId: 9)
|
||||
|
||||
XCTAssertEqual(api.albumFolderListRequests.first?.name, "旅拍")
|
||||
XCTAssertEqual(api.albumFolderListRequests.first?.startTime, "2026-06-01")
|
||||
XCTAssertEqual(api.albumFolderListRequests.last?.page, 2)
|
||||
XCTAssertEqual(viewModel.folders.map(\.id), [1, 2])
|
||||
}
|
||||
|
||||
/// 测试新建相册成功后刷新列表。
|
||||
func testAlbumCreateRefreshesList() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFolderPages = [ListPayload(total: 1, list: [AlbumFolderItem.fixture(id: 3, name: "新相册")])]
|
||||
let viewModel = AlbumListViewModel()
|
||||
|
||||
let success = await viewModel.createFolder(name: " 新相册 ", remark: " 备注 ", scenicId: 9, api: api)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(api.albumAddRequests.first?.name, "新相册")
|
||||
XCTAssertEqual(api.albumAddRequests.first?.remark, "备注")
|
||||
XCTAssertEqual(viewModel.folders.first?.id, 3)
|
||||
}
|
||||
|
||||
/// 测试相册详情加载信息和文件列表,切换 Tab 会重置分页。
|
||||
func testAlbumDetailLoadsInfoAndSwitchesTab() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFilePages = [
|
||||
ListPayload(total: 1, list: [AlbumFileItem.fixture(id: 11, fileType: 2)]),
|
||||
ListPayload(total: 1, list: [AlbumFileItem.fixture(id: 12, fileType: 1)])
|
||||
]
|
||||
let viewModel = AlbumDetailViewModel(folderId: 3)
|
||||
|
||||
await viewModel.reload(api: api, scenicId: 9)
|
||||
await viewModel.selectTab(.video, api: api, scenicId: 9)
|
||||
|
||||
XCTAssertEqual(api.albumInfoRequests, [3, 3])
|
||||
XCTAssertEqual(api.albumFileListRequests.map { $0.fileType }, [2, 1])
|
||||
XCTAssertEqual(viewModel.files.first?.id, 12)
|
||||
XCTAssertEqual(viewModel.page, 1)
|
||||
}
|
||||
|
||||
/// 测试相册详情删除、设置封面和编辑信息会调用正确接口。
|
||||
func testAlbumDetailMutationsUseExpectedRequests() async {
|
||||
let api = MockAssetsService()
|
||||
let viewModel = AlbumDetailViewModel(folderId: 3)
|
||||
let file = AlbumFileItem.fixture(id: 11, fileType: 2)
|
||||
|
||||
_ = await viewModel.delete(file: file, scenicId: 9, api: api)
|
||||
_ = await viewModel.setCover(file: file, scenicId: 9, api: api)
|
||||
_ = await viewModel.rename(name: " 改名 ", scenicId: 9, api: api)
|
||||
_ = await viewModel.updateRemark(" 新备注 ", scenicId: 9, api: api)
|
||||
|
||||
XCTAssertEqual(api.albumDeleteRequests.first?.idList, [11])
|
||||
XCTAssertEqual(api.albumEditRequests[0].coverFileId, 11)
|
||||
XCTAssertEqual(api.albumEditRequests[1].name, "改名")
|
||||
XCTAssertEqual(api.albumEditRequests[2].remark, "新备注")
|
||||
}
|
||||
|
||||
/// 测试相册预览上传会先上传 OSS,再登记相册文件。
|
||||
func testAlbumTrailerUploadUploadsOSSBeforeRegisteringFile() async {
|
||||
let api = MockAssetsService()
|
||||
api.albumFolderPages = [ListPayload(total: 1, list: [AlbumFolderItem.fixture(id: 3, name: "旅拍")])]
|
||||
let uploader = MockAssetsUploader()
|
||||
let viewModel = AlbumTrailerViewModel()
|
||||
viewModel.addLocalFiles([AlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)])
|
||||
await viewModel.loadFolders(api: api, scenicId: 9)
|
||||
|
||||
let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["a.jpg"])
|
||||
XCTAssertEqual(api.albumUploadURLRequests.first?.fileURL, "https://cdn.example.com/a.jpg")
|
||||
XCTAssertTrue(viewModel.localFiles.isEmpty)
|
||||
XCTAssertEqual(viewModel.uploadProgress, 100)
|
||||
}
|
||||
|
||||
/// 测试相册上传失败时不会登记文件入库。
|
||||
func testAlbumTrailerUploadFailureDoesNotRegisterFile() async {
|
||||
let api = MockAssetsService()
|
||||
let uploader = MockAssetsUploader()
|
||||
uploader.shouldFail = true
|
||||
let viewModel = AlbumTrailerViewModel()
|
||||
viewModel.selectedFolderId = 3
|
||||
viewModel.addLocalFiles([AlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)])
|
||||
|
||||
let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(api.albumUploadURLRequests.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 资产服务测试替身,记录请求并返回预设数据。
|
||||
@MainActor
|
||||
private final class MockAssetsService: AssetsServing {
|
||||
struct CloudListRequest: Equatable {
|
||||
let parentFolderId: Int
|
||||
let name: String
|
||||
let type: Int
|
||||
let orderBy: Int
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
}
|
||||
|
||||
struct MediaListRequest: Equatable {
|
||||
let kind: MediaLibraryKind
|
||||
let keyword: String
|
||||
let status: Int?
|
||||
let page: Int
|
||||
}
|
||||
|
||||
struct ProjectListRequest: Equatable {
|
||||
let scenicId: Int
|
||||
let name: String?
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
}
|
||||
|
||||
struct AlbumFolderListRequest: Equatable {
|
||||
let scenicId: Int
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
let name: String
|
||||
let startTime: String?
|
||||
let endTime: String?
|
||||
}
|
||||
|
||||
struct AlbumUploadURLRequest: Equatable {
|
||||
let scenicId: Int
|
||||
let fileURL: String
|
||||
let folderId: Int
|
||||
}
|
||||
|
||||
var cloudPages: [ListPayload<CloudDriveFile>] = [ListPayload(total: 0, list: [])]
|
||||
var mediaListResponses: [MediaLibraryListResponse] = [MediaLibraryListResponse.fixture(total: 0, ids: [])]
|
||||
var projectPages: [ListPayload<PhotographerProjectItem>] = [ListPayload(total: 0, list: [])]
|
||||
var albumFolderPages: [ListPayload<AlbumFolderItem>] = [ListPayload(total: 0, list: [])]
|
||||
var albumFilePages: [ListPayload<AlbumFileItem>] = [ListPayload(total: 0, list: [])]
|
||||
var uploadPermission = true
|
||||
var uploadReason = ""
|
||||
var cloudListRequests: [CloudListRequest] = []
|
||||
var cloudUploadRequests: [CloudFileUploadRequest] = []
|
||||
var mediaListRequests: [MediaListRequest] = []
|
||||
var mediaOperationRequests: [MediaAlbumOperationRequest] = []
|
||||
var mediaUploadRequests: [MediaAlbumUploadRequest] = []
|
||||
var mediaEditRequests: [MediaAlbumEditRequest] = []
|
||||
var projectListRequests: [ProjectListRequest] = []
|
||||
var albumFolderListRequests: [AlbumFolderListRequest] = []
|
||||
var albumInfoRequests: [Int] = []
|
||||
var albumFileListRequests: [(scenicId: Int, folderId: Int, fileType: Int, page: Int)] = []
|
||||
var albumAddRequests: [(scenicId: Int, name: String, remark: String)] = []
|
||||
var albumEditRequests: [AlbumFolderEditRequest] = []
|
||||
var albumDeleteRequests: [AlbumFileDeleteRequest] = []
|
||||
var albumUploadURLRequests: [AlbumUploadURLRequest] = []
|
||||
|
||||
/// 返回云盘文件列表。
|
||||
func cloudFileList(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int) async throws -> ListPayload<CloudDriveFile> {
|
||||
cloudListRequests.append(CloudListRequest(parentFolderId: parentFolderId, name: name, type: type, orderBy: orderBy, page: page, pageSize: pageSize))
|
||||
return cloudPages.isEmpty ? ListPayload(total: 0, list: []) : cloudPages.removeFirst()
|
||||
}
|
||||
|
||||
/// 创建文件夹。
|
||||
func cloudFolderCreate(_ request: CloudFolderCreateRequest) async throws {}
|
||||
|
||||
/// 登记云盘文件。
|
||||
func cloudFileUpload(_ request: CloudFileUploadRequest) async throws {
|
||||
cloudUploadRequests.append(request)
|
||||
}
|
||||
|
||||
/// 删除云盘文件。
|
||||
func cloudFileDelete(_ request: CloudFileDeleteRequest) async throws {}
|
||||
|
||||
/// 移动云盘文件。
|
||||
func cloudFileMove(_ request: CloudFileMoveRequest) async throws {}
|
||||
|
||||
/// 重命名文件夹。
|
||||
func cloudFolderEdit(_ request: CloudFolderModifyRequest) async throws {}
|
||||
|
||||
/// 重命名文件。
|
||||
func cloudFileEdit(_ request: CloudFileModifyRequest) async throws {}
|
||||
|
||||
/// 检查上传权限。
|
||||
func checkCloudUploadPermission() async throws -> CheckCloudUploadResponse {
|
||||
try JSONDecoder().decode(CheckCloudUploadResponse.self, from: #"{"can_upload":\#(uploadPermission ? 1 : 0),"reason":"\#(uploadReason)"}"#.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
/// 返回素材列表。
|
||||
func mediaAlbumList(kind: MediaLibraryKind, keyword: String, status: Int?, page: Int, pageSize: Int) async throws -> MediaLibraryListResponse {
|
||||
mediaListRequests.append(MediaListRequest(kind: kind, keyword: keyword, status: status, page: page))
|
||||
return mediaListResponses.isEmpty ? MediaLibraryListResponse.fixture(total: 0, ids: []) : mediaListResponses.removeFirst()
|
||||
}
|
||||
|
||||
/// 返回素材详情。
|
||||
func mediaAlbumDetail(id: Int, kind: MediaLibraryKind) async throws -> MediaLibraryDetail {
|
||||
MediaLibraryDetail.fixture(id: id)
|
||||
}
|
||||
|
||||
/// 更新上下架。
|
||||
func mediaAlbumOperation(_ request: MediaAlbumOperationRequest) async throws {
|
||||
mediaOperationRequests.append(request)
|
||||
}
|
||||
|
||||
/// 删除素材。
|
||||
func mediaAlbumDelete(_ request: MediaAlbumDeleteRequest) async throws {}
|
||||
|
||||
/// 新增标签。
|
||||
func mediaAlbumAddTag(_ request: MediaAlbumAddTagRequest) async throws {}
|
||||
|
||||
/// 上传素材。
|
||||
func mediaAlbumUpload(_ request: MediaAlbumUploadRequest) async throws {
|
||||
mediaUploadRequests.append(request)
|
||||
}
|
||||
|
||||
/// 编辑素材。
|
||||
func mediaAlbumEdit(_ request: MediaAlbumEditRequest) async throws {
|
||||
mediaEditRequests.append(request)
|
||||
}
|
||||
|
||||
/// 返回样片可关联项目列表。
|
||||
func projectList(scenicId: Int, name: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
|
||||
projectListRequests.append(ProjectListRequest(scenicId: scenicId, name: name, page: page, pageSize: pageSize))
|
||||
return projectPages.isEmpty ? ListPayload(total: 0, list: []) : projectPages.removeFirst()
|
||||
}
|
||||
|
||||
/// 返回相册文件夹列表。
|
||||
func albumFolderList(scenicId: Int, page: Int, pageSize: Int, name: String, startTime: String?, endTime: String?) async throws -> ListPayload<AlbumFolderItem> {
|
||||
albumFolderListRequests.append(AlbumFolderListRequest(scenicId: scenicId, page: page, pageSize: pageSize, name: name, startTime: startTime, endTime: endTime))
|
||||
return albumFolderPages.isEmpty ? ListPayload(total: 0, list: []) : albumFolderPages.removeFirst()
|
||||
}
|
||||
|
||||
/// 返回相册文件夹详情。
|
||||
func albumFolderInfo(id: Int) async throws -> AlbumFolderItem {
|
||||
albumInfoRequests.append(id)
|
||||
return AlbumFolderItem.fixture(id: id, name: "相册\(id)")
|
||||
}
|
||||
|
||||
/// 返回相册文件列表。
|
||||
func albumFileList(scenicId: Int, folderId: Int, fileType: Int, page: Int, pageSize: Int) async throws -> ListPayload<AlbumFileItem> {
|
||||
albumFileListRequests.append((scenicId: scenicId, folderId: folderId, fileType: fileType, page: page))
|
||||
return albumFilePages.isEmpty ? ListPayload(total: 0, list: []) : albumFilePages.removeFirst()
|
||||
}
|
||||
|
||||
/// 新建相册。
|
||||
func addAlbumFolder(scenicId: Int, name: String, remark: String) async throws {
|
||||
albumAddRequests.append((scenicId: scenicId, name: name, remark: remark))
|
||||
}
|
||||
|
||||
/// 编辑相册。
|
||||
func editAlbumFolder(folderId: Int, coverFileId: Int?, name: String?, remark: String?) async throws {
|
||||
albumEditRequests.append(AlbumFolderEditRequest(folderId: folderId, coverFileId: coverFileId, name: name, remark: remark))
|
||||
}
|
||||
|
||||
/// 删除相册文件。
|
||||
func deleteAlbumFiles(folderId: Int, idList: [Int]) async throws {
|
||||
albumDeleteRequests.append(AlbumFileDeleteRequest(idList: idList, folderId: folderId))
|
||||
}
|
||||
|
||||
/// 登记相册上传文件。
|
||||
func albumFileUploadURL(scenicId: Int, fileURL: String, folderId: Int) async throws {
|
||||
albumUploadURLRequests.append(AlbumUploadURLRequest(scenicId: scenicId, fileURL: fileURL, folderId: folderId))
|
||||
}
|
||||
}
|
||||
|
||||
/// OSS 上传测试替身,记录上传文件并返回固定 CDN URL。
|
||||
@MainActor
|
||||
private final class MockAssetsUploader: OSSUploadServing {
|
||||
struct UploadedFile: Equatable {
|
||||
let fileName: String
|
||||
let fileType: Int
|
||||
let scenicId: Int
|
||||
}
|
||||
|
||||
var uploadedFiles: [UploadedFile] = []
|
||||
var shouldFail = false
|
||||
|
||||
/// 上传用户头像。
|
||||
func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传实名认证图片。
|
||||
func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传云盘文件。
|
||||
func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: fileType, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传相册文件。
|
||||
func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: fileType, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传任务附件。
|
||||
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: fileType, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传项目图片。
|
||||
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传打卡点图片。
|
||||
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传景区申请图片。
|
||||
func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传银行卡图片。
|
||||
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 通用测试上传流程。
|
||||
private func upload(fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
if shouldFail {
|
||||
throw APIError.networkFailed("上传失败")
|
||||
}
|
||||
uploadedFiles.append(UploadedFile(fileName: fileName, fileType: fileType, scenicId: scenicId))
|
||||
onProgress(100)
|
||||
return "https://cdn.example.com/\(fileName)"
|
||||
}
|
||||
}
|
||||
|
||||
private extension MediaLibraryListResponse {
|
||||
/// 创建素材列表测试数据。
|
||||
static func fixture(total: Int, ids: [Int]) -> MediaLibraryListResponse {
|
||||
let list = ids.map { MediaLibraryItem.fixture(id: $0, status: 1, listingStatus: 0) }
|
||||
let payload = [
|
||||
"total": total,
|
||||
"list": list.map { ["id": $0.id, "name": $0.name, "status": $0.status, "listing_status": $0.listingStatus] },
|
||||
"order": ["total_num": 0, "avg_order_amount": 0, "refund_total": 0, "avg_change": 0]
|
||||
] as [String: Any]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(MediaLibraryListResponse.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MediaLibraryItem {
|
||||
/// 创建素材列表项测试数据。
|
||||
static func fixture(id: Int, status: Int, listingStatus: Int) -> MediaLibraryItem {
|
||||
let payload: [String: Any] = [
|
||||
"id": id,
|
||||
"name": "素材\(id)",
|
||||
"cover_url": "",
|
||||
"created_at": "",
|
||||
"status": status,
|
||||
"download_count": 0,
|
||||
"likes_count": 0,
|
||||
"collect_count": 0,
|
||||
"share_count": 0,
|
||||
"scenic_spot_name": "",
|
||||
"project_name": "",
|
||||
"listing_status": listingStatus
|
||||
]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(MediaLibraryItem.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MediaLibraryDetail {
|
||||
/// 创建素材详情测试数据。
|
||||
static func fixture(id: Int) -> MediaLibraryDetail {
|
||||
let payload: [String: Any] = [
|
||||
"id": id,
|
||||
"name": "素材\(id)",
|
||||
"cover_url": "",
|
||||
"description": "",
|
||||
"status": 1,
|
||||
"listing_status": 0,
|
||||
"likes_count": 0,
|
||||
"download_count": 0,
|
||||
"collect_count": 0,
|
||||
"share_count": 0,
|
||||
"uploader_name": "",
|
||||
"created_at": "",
|
||||
"media_list": [],
|
||||
"scenic_name": "",
|
||||
"project_name": ""
|
||||
]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(MediaLibraryDetail.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AlbumFolderItem {
|
||||
/// 创建相册文件夹测试数据。
|
||||
static func fixture(id: Int, name: String) -> AlbumFolderItem {
|
||||
let payload: [String: Any] = [
|
||||
"id": id,
|
||||
"name": name,
|
||||
"cover_file_id": 0,
|
||||
"count_video": 1,
|
||||
"count_image": 2,
|
||||
"created_at": "2026",
|
||||
"remark": "备注"
|
||||
]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(AlbumFolderItem.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AlbumFileItem {
|
||||
/// 创建相册文件测试数据。
|
||||
static func fixture(id: Int, fileType: Int) -> AlbumFileItem {
|
||||
let fileName = fileType == 1 ? "v.mp4" : "a.jpg"
|
||||
let payload: [String: Any] = [
|
||||
"id": id,
|
||||
"file_name": fileName,
|
||||
"file_type": fileType,
|
||||
"file_url": "https://cdn.example.com/\(fileName)",
|
||||
"cover_url": "",
|
||||
"file_size": "2048",
|
||||
"file_size_human": "2KB",
|
||||
"remark": ""
|
||||
]
|
||||
let data = try! JSONSerialization.data(withJSONObject: payload)
|
||||
return try! JSONDecoder().decode(AlbumFileItem.self, from: data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user