Files
suixinkan_ios_new/suixinkanTests/Assets/AssetsViewModelTests.swift
汉秋 cac22fcc56 新增资产模块,包含云存储与媒体库流程
为云端与素材管理页面接入首页路由,将共享云存储模型迁入 Assets 模块,并补充 API/ViewModel 测试。

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

365 lines
15 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.

//
// AssetsViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/23.
//
import XCTest
@testable import suixinkan
@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 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, "审核通过后才能上下架")
}
/// 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)
}
}
///
@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 keyword: String
let status: Int?
let page: Int
}
var cloudPages: [ListPayload<CloudDriveFile>] = [ListPayload(total: 0, list: [])]
var mediaListResponses: [MediaLibraryListResponse] = [MediaLibraryListResponse.fixture(total: 0, ids: [])]
var uploadPermission = true
var uploadReason = ""
var cloudListRequests: [CloudListRequest] = []
var cloudUploadRequests: [CloudFileUploadRequest] = []
var mediaListRequests: [MediaListRequest] = []
var mediaOperationRequests: [MediaAlbumOperationRequest] = []
var mediaUploadRequests: [MediaAlbumUploadRequest] = []
var mediaEditRequests: [MediaAlbumEditRequest] = []
///
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(keyword: keyword, status: status, page: page))
return mediaListResponses.isEmpty ? MediaLibraryListResponse.fixture(total: 0, ids: []) : mediaListResponses.removeFirst()
}
///
func mediaAlbumDetail(id: Int) 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)
}
}
/// 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 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)
}
}