// // WiredCameraTransferViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/30. // import XCTest @testable import suixinkan @MainActor /// 有线传图 ViewModel 测试。 final class WiredCameraTransferViewModelTests: XCTestCase { /// 测试相机管道回调的首张新照片应出现在列表中。 func testFirstNewPipelineTaskAppearsInPhotos() async throws { let camera = MockWiredCameraService() let context = WiredTransferContext( albumId: 1, albumName: "测试相册", phone: "", orderNumber: "" ) let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera) let account = AccountContext() account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试")) await viewModel.start( api: MockTravelAlbumAPIForWiredTransfer(), ossService: WiredTransferMockOSSUploadService(), scenicID: 1, accountContext: account ) let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_NEW.JPG") try Data(repeating: 0xFF, count: 16).write(to: tempURL) defer { try? FileManager.default.removeItem(at: tempURL) } let asset = CameraAsset(id: "ptp_new_001", filename: "DSC_NEW.JPG", fileSize: 16) camera.downloadURL = tempURL camera.onNewAsset?(asset) try await Task.sleep(nanoseconds: 500_000_000) XCTAssertEqual(viewModel.photos.count, 1) XCTAssertEqual(viewModel.photos.first?.id, "ptp_new_001") XCTAssertEqual(viewModel.photos.first?.fileName, "DSC_NEW.JPG") } /// 测试绑定到其他相册的照片不会出现在当前相册列表。 func testPhotoBoundToOtherAlbumIsFilteredOut() async throws { let bindingStore = WiredTransferPhotoStore( accountPrefixProvider: { "user_101" }, userIDProvider: { "101" } ) bindingStore.bindPhotoToAlbum(photoID: "ptp_other_album", albumID: 99) defer { bindingStore.bindPhotoToAlbum(photoID: "ptp_other_album", albumID: 1) } let camera = MockWiredCameraService() let context = WiredTransferContext( albumId: 1, albumName: "当前相册", phone: "", orderNumber: "" ) let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera) let account = AccountContext() account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"), accountType: "user") await viewModel.start( api: MockTravelAlbumAPIForWiredTransfer(), ossService: WiredTransferMockOSSUploadService(), scenicID: 1, accountContext: account ) let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_OTHER.JPG") try Data(repeating: 0xAA, count: 8).write(to: tempURL) defer { try? FileManager.default.removeItem(at: tempURL) } camera.downloadURL = tempURL camera.onNewAsset?(CameraAsset(id: "ptp_other_album", filename: "DSC_OTHER.JPG", fileSize: 8)) try await Task.sleep(nanoseconds: 500_000_000) XCTAssertTrue(viewModel.photos.isEmpty) } } @MainActor private final class MockWiredCameraService: CameraServiceProtocol { let brand: CameraBrand = .sony var connectionState: CameraConnectionState = .connected(deviceName: "Mock") var onConnectionStateChange: ((CameraConnectionState) -> Void)? var onNewAsset: ((CameraAsset) -> Void)? var downloadURL: URL? func connect() async {} func disconnect() async {} func listAssets() async throws -> [CameraAsset] { [] } func downloadAsset(_ asset: CameraAsset) async throws -> URL { if let downloadURL { return downloadURL } let url = FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename) if !FileManager.default.fileExists(atPath: url.path) { try Data(repeating: 0xFF, count: 8).write(to: url) } return url } } @MainActor private final class MockTravelAlbumAPIForWiredTransfer: TravelAlbumServing { func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] } func createAlbum(_ request: TravelAlbumCreateRequest) async throws -> TravelAlbumCreateResponse { TravelAlbumCreateResponse(id: 1) } func albumList(page: Int, pageSize: Int) async throws -> ListPayload { ListPayload(total: 0, list: []) } func albumInfo(id: Int) async throws -> TravelAlbumItem { throw APIError.emptyData } func editAlbum(_ request: TravelAlbumEditRequest) async throws {} func deleteAlbum(_ request: TravelAlbumDeleteRequest) async throws {} func materialList(userEquityTravelId: Int, page: Int, pageSize: Int) async throws -> ListPayload { ListPayload(total: 0, list: []) } func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial { let data = Data( #"{"code":100000,"msg":"success","data":{"id":"11","user_equity_travel_id":"8","status":"1","order_number":"","user_id":"100","file_name":"DSC_001.JPG","file_type":"2","file_url":"https://cdn/a.jpg","file_size":"2048","cover_url":"","is_purchased":"0","created_at":"2026","updated_at":"2026"}}"#.utf8 ) let envelope = try JSONDecoder().decode(APIEnvelope.self, from: data) return try XCTUnwrap(envelope.data) } func deleteMaterial(_ request: TravelAlbumDeleteMaterialRequest) async throws {} func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse { TravelAlbumMpCodeResponse(mpCodeOssURL: "") } } @MainActor private final class WiredTransferMockOSSUploadService: OSSUploadServing { func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { onProgress(100) return "https://cdn/mock/\(fileName)" } func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" } }