// // SampleManagementViewModelTests.swift // suixinkanTests // import Foundation import XCTest @testable import suixinkan @MainActor /// 样片管理 ViewModel 测试。 final class SampleManagementViewModelTests: XCTestCase { func testListLoadsRefreshesAndMapsFilterStatus() async { let api = MockSampleAPI() api.listResponses = [ SampleMaterialListResponse(total: 12, list: (0 ..< 10).map { makeItem(id: $0) }), SampleMaterialListResponse(total: 12, list: [makeItem(id: 10), makeItem(id: 11)]), SampleMaterialListResponse(total: 1, list: [makeItem(id: 99)]), ] let viewModel = SampleListViewModel() await viewModel.loadInitial(api: api) await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api) await viewModel.selectFilter(.pending, api: api) XCTAssertEqual(api.listCalls.map(\.status), [4, 4, 0]) XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1]) XCTAssertEqual(viewModel.items.map(\.id), [99]) } func testRejectedSampleCannotToggleListing() async { let api = MockSampleAPI() api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 1, status: 0, listingStatus: 0)])] let viewModel = SampleListViewModel() var messages: [String] = [] viewModel.onShowMessage = { messages.append($0) } await viewModel.loadInitial(api: api) await viewModel.toggleListing(sampleId: 1, checked: true, api: api) XCTAssertEqual(messages.last, "审核通过才可以操作上下架") XCTAssertTrue(api.operationCalls.isEmpty) } func testApprovedSampleToggleUpdatesLocalItem() async { let api = MockSampleAPI() api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 2, status: 1, listingStatus: 0)])] let viewModel = SampleListViewModel() await viewModel.loadInitial(api: api) await viewModel.toggleListing(sampleId: 2, checked: true, api: api) XCTAssertEqual(api.operationCalls.first?.id, 2) XCTAssertEqual(api.operationCalls.first?.listingStatus, 1) XCTAssertEqual(viewModel.items.first?.listingStatus, 1) } func testUploadValidationMessages() async { let api = MockSampleAPI() let viewModel = UploadSampleViewModel( scenicListProvider: { [ScenicInfo(id: 3, name: "景区", status: 1, coverImg: nil)] }, currentScenicIdProvider: { 3 } ) var messages: [String] = [] viewModel.onShowMessage = { messages.append($0) } await viewModel.uploadSample(api: api) viewModel.updateAlbumName("样片") await viewModel.uploadSample(api: api) await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg", ossUrl: "")], uploader: MockSampleUploader(urls: ["https://cdn/a.jpg"])) await viewModel.uploadSample(api: api) await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg", ossUrl: ""), uploader: MockSampleUploader(urls: ["https://cdn/c.jpg"])) await viewModel.uploadSample(api: api) XCTAssertEqual(messages.prefix(4), [ "请输入相册名称", "请至少上传一个素材", "请上传封面图片", "请选择关联景区", ]) XCTAssertNil(api.lastUploadRequest) } func testUploadSuccessBuildsSampleRequest() async { let scenic = ScenicInfo(id: 3, name: "景区", status: 1, coverImg: nil) let api = MockSampleAPI() api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1) let viewModel = UploadSampleViewModel( scenicListProvider: { [scenic] }, currentScenicIdProvider: { 3 } ) let uploader = MockSampleUploader(urls: ["https://cdn/a.jpg", "https://cdn/c.jpg"]) var didSucceed = false viewModel.onUploadSuccess = { didSucceed = true } viewModel.updateAlbumName("样片") await viewModel.selectScenic(scenic, api: api) viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点")) viewModel.selectProject(SampleProjectItem(id: 7, name: "项目")) await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader) await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: uploader) await viewModel.uploadSample(api: api) XCTAssertTrue(didSucceed) XCTAssertEqual(api.lastUploadRequest?.name, "样片") XCTAssertEqual(api.lastUploadRequest?.type, "2") XCTAssertEqual(api.lastUploadRequest?.mediaType, "1") XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/a.jpg") XCTAssertEqual(api.lastUploadRequest?.coverUrl, "https://cdn/c.jpg") XCTAssertEqual(api.lastUploadRequest?.scenicSpotId, 9) XCTAssertEqual(api.lastUploadRequest?.projectId, 7) } } @MainActor private final class MockSampleAPI: SampleManagementServing { var listResponses: [SampleMaterialListResponse] = [] var listCalls: [(status: Int, keyword: String?, page: Int, pageSize: Int)] = [] var operationCalls: [(id: Int, listingStatus: Int)] = [] var lastUploadRequest: SampleUploadMaterialRequest? var spotResponse = ScenicSpotListResponse(list: [], total: 0) var projectResponse = SampleProjectListResponse(list: [], total: 0) func list(status: Int, keyword: String?, page: Int, pageSize: Int) async throws -> SampleMaterialListResponse { listCalls.append((status, keyword, page, pageSize)) guard !listResponses.isEmpty else { return SampleMaterialListResponse() } return listResponses.removeFirst() } func detail(id: Int) async throws -> SampleDetail { throw APIError.emptyData } func setListingStatus(id: Int, listingStatus: Int) async throws { operationCalls.append((id, listingStatus)) } func upload(_ request: SampleUploadMaterialRequest) async throws { lastUploadRequest = request } func scenicSpotListAll(scenicId: String) async throws -> ScenicSpotListResponse { spotResponse } func projectList(scenicId: String, name: String?, page: Int, pageSize: Int) async throws -> SampleProjectListResponse { projectResponse } } @MainActor private final class MockSampleUploader: SampleOSSUploading { private var urls: [String] init(urls: [String]) { self.urls = urls } func uploadSampleFile( data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void ) async throws -> String { onProgress(100) return urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst() } } private func makeItem(id: Int, status: Int = 1, listingStatus: Int = 0) -> SampleMaterialItem { SampleMaterialItem( id: id, name: "样片\(id)", coverUrl: "", createdAt: "", status: status, downloadCount: 0, likesCount: 0, collectCount: 0, shareCount: 0, scenicSpotName: "打卡点", projectName: "项目", listingStatus: listingStatus ) } private func makeUploadItem(fileName: String, ossUrl: String = "") -> SampleUploadedMediaItem { SampleUploadedMediaItem( data: Data(repeating: 1, count: 10), fileName: fileName, size: 10, width: 1920, height: 1080, ossUrl: ossUrl ) }