Files
suixinkan_ios_new/suixinkanTests/Assets/AssetsAPITests.swift
汉秋 403a3eefa6 为资产模块新增样片管理与上传流程
扩展媒体库以支持样片类型及项目选择,为样片入口接入首页路由,并扩展 Assets 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:06:16 +08:00

315 lines
17 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.

//
// AssetsAPITests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/23.
//
import XCTest
@testable import suixinkan
@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])
}