新增资产模块,包含云存储与媒体库流程
为云端与素材管理页面接入首页路由,将共享云存储模型迁入 Assets 模块,并补充 API/ViewModel 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
188
suixinkanTests/Assets/AssetsAPITests.swift
Normal file
188
suixinkanTests/Assets/AssetsAPITests.swift
Normal file
@ -0,0 +1,188 @@
|
||||
//
|
||||
// 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)
|
||||
|
||||
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(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, listingStatus: 1))
|
||||
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 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")
|
||||
}
|
||||
|
||||
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)!
|
||||
}
|
||||
|
||||
/// 资产 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])
|
||||
}
|
||||
Reference in New Issue
Block a user