移除 Core 层 OTG 相机实现,保留旅拍有线传图 UI 与本地照片上传能力。
将本地存储与上传编排下沉到 Features/TravelAlbum,删除 USB/PTP 相关代码与测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
76
suixinkanTests/TravelAlbum/CameraDownloadStorageTests.swift
Normal file
76
suixinkanTests/TravelAlbum/CameraDownloadStorageTests.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// CameraDownloadStorageTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// CameraDownloadStorage 相对路径持久化测试。
|
||||
final class CameraDownloadStorageTests: XCTestCase {
|
||||
/// 测试 scoped 原图相对路径前缀。
|
||||
func testRelativePathUsesScopedOriginalsPrefix() {
|
||||
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
let fileURL = CameraDownloadStorage.originalsDirectory(for: scope).appendingPathComponent("1730_test.JPG")
|
||||
XCTAssertEqual(
|
||||
CameraDownloadStorage.relativePath(for: fileURL),
|
||||
"CameraDownloads/user_101/6603/originals/1730_test.JPG"
|
||||
)
|
||||
}
|
||||
|
||||
/// 测试相对路径解析。
|
||||
func testResolveLocalURLFromRelativePath() {
|
||||
let stored = "CameraDownloads/user_101/6603/originals/1730_test.JPG"
|
||||
let resolved = CameraDownloadStorage.resolveLocalURL(from: stored)
|
||||
let expected = CameraDownloadStorage.documentsDirectory.appendingPathComponent(stored)
|
||||
XCTAssertEqual(resolved, expected)
|
||||
}
|
||||
|
||||
/// 测试 scoped 目录路径。
|
||||
func testScopeDirectoryPath() {
|
||||
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
let directory = CameraDownloadStorage.scopeDirectory(for: scope)
|
||||
XCTAssertTrue(directory.path.hasSuffix("CameraDownloads/user_101/6603"))
|
||||
}
|
||||
|
||||
/// 测试唯一原图 URL 生成。
|
||||
func testUniqueLocalURLUsesScopedOriginalsDirectory() {
|
||||
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
let url = CameraDownloadStorage.uniqueLocalURL(for: "DSC.JPG", scope: scope)
|
||||
XCTAssertTrue(url.path.contains("/CameraDownloads/user_101/6603/originals/"))
|
||||
XCTAssertTrue(url.lastPathComponent.hasSuffix("_DSC.JPG"))
|
||||
}
|
||||
|
||||
/// 测试删除 scoped 目录。
|
||||
func testRemoveScopeDirectory() throws {
|
||||
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
|
||||
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "remove_test.JPG", scope: scope)
|
||||
try Data(repeating: 0xFF, count: 8).write(to: fileURL)
|
||||
|
||||
let directory = CameraDownloadStorage.scopeDirectory(for: scope)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: directory.path))
|
||||
|
||||
CameraDownloadStorage.removeScopeDirectory(for: scope)
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: directory.path))
|
||||
}
|
||||
|
||||
/// 测试预览 URL 字符串。
|
||||
func testPreviewURLStringFromExistingFile() throws {
|
||||
let scope = CameraDownloadStorage.Scope(accountKey: "user_101", albumID: 6603)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "preview.JPG", scope: scope)
|
||||
try Data(repeating: 0xFF, count: 8).write(to: fileURL)
|
||||
|
||||
let stored = CameraDownloadStorage.relativePath(for: fileURL)
|
||||
let preview = CameraDownloadStorage.previewURLString(from: stored)
|
||||
XCTAssertFalse(preview.isEmpty)
|
||||
XCTAssertTrue(preview.hasPrefix("file://"))
|
||||
}
|
||||
}
|
||||
@ -12,102 +12,15 @@ import UIKit
|
||||
@MainActor
|
||||
/// 有线传图 ViewModel 测试。
|
||||
final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
/// 测试相机管道回调的首张新照片应出现在列表中。
|
||||
func testFirstNewPipelineTaskAppearsInPhotos() async throws {
|
||||
let camera = MockWiredCameraService()
|
||||
/// 测试 start 被取消时会 detach UI 回调。
|
||||
func testStartCancellationDetachesFromUI() async {
|
||||
let context = WiredTransferContext(
|
||||
albumId: 6603,
|
||||
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 testDeletedPendingPhotoDoesNotReappearAfterLiveCapture() async throws {
|
||||
let camera = MockWiredCameraService()
|
||||
let context = WiredTransferContext(
|
||||
albumId: 7701,
|
||||
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 oldURL = FileManager.default.temporaryDirectory.appendingPathComponent("OLD_DELETE.JPG")
|
||||
try Data(repeating: 0x11, count: 16).write(to: oldURL)
|
||||
defer { try? FileManager.default.removeItem(at: oldURL) }
|
||||
|
||||
camera.downloadURL = oldURL
|
||||
camera.onNewAsset?(CameraAsset(id: "ptp_delete_me", filename: "OLD_DELETE.JPG", fileSize: 16))
|
||||
try await Task.sleep(nanoseconds: 500_000_000)
|
||||
XCTAssertEqual(viewModel.photos.count, 1)
|
||||
|
||||
viewModel.deletePhoto(id: "ptp_delete_me")
|
||||
XCTAssertTrue(viewModel.photos.isEmpty)
|
||||
|
||||
viewModel.selectTransferMode(
|
||||
WiredCameraTransferViewModel.modeLiveCapture,
|
||||
api: MockTravelAlbumAPIForWiredTransfer(),
|
||||
ossService: WiredTransferMockOSSUploadService(),
|
||||
scenicID: 1
|
||||
)
|
||||
|
||||
let newURL = FileManager.default.temporaryDirectory.appendingPathComponent("NEW_LIVE.JPG")
|
||||
try Data(repeating: 0x22, count: 16).write(to: newURL)
|
||||
defer { try? FileManager.default.removeItem(at: newURL) }
|
||||
|
||||
camera.downloadURL = newURL
|
||||
camera.onNewAsset?(CameraAsset(id: "ptp_live_new", filename: "NEW_LIVE.JPG", fileSize: 16))
|
||||
try await Task.sleep(nanoseconds: 500_000_000)
|
||||
|
||||
XCTAssertEqual(viewModel.photos.count, 1)
|
||||
XCTAssertEqual(viewModel.photos.first?.id, "ptp_live_new")
|
||||
XCTAssertFalse(viewModel.photos.contains { $0.id == "ptp_delete_me" })
|
||||
}
|
||||
|
||||
/// 测试 start 被取消时 detach UI 回调,但不 shutdown 相机连接。
|
||||
func testStartCancellationDetachesWithoutShutdown() async {
|
||||
let camera = DisconnectTrackingWiredCameraService()
|
||||
let context = WiredTransferContext(
|
||||
albumId: 6603,
|
||||
albumName: "测试相册",
|
||||
phone: "",
|
||||
orderNumber: ""
|
||||
)
|
||||
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
|
||||
let viewModel = WiredCameraTransferViewModel(context: context)
|
||||
let account = AccountContext()
|
||||
account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))
|
||||
|
||||
@ -120,54 +33,11 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
for _ in 0 ..< 100 {
|
||||
if camera.connectCount > 0 { break }
|
||||
try? await Task.sleep(nanoseconds: 10_000_000)
|
||||
}
|
||||
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
startTask.cancel()
|
||||
try? await Task.sleep(nanoseconds: 300_000_000)
|
||||
try? await Task.sleep(nanoseconds: 200_000_000)
|
||||
|
||||
XCTAssertGreaterThanOrEqual(camera.detachCount, 1)
|
||||
XCTAssertEqual(camera.shutdownCount, 0)
|
||||
}
|
||||
|
||||
/// 测试绑定到其他相册的照片不会出现在当前相册列表。
|
||||
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)
|
||||
XCTAssertEqual(viewModel.connectionState, .disconnected)
|
||||
}
|
||||
|
||||
/// 测试批量上传按钮三态流转。
|
||||
@ -209,8 +79,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
let today = formatter.string(from: Date())
|
||||
|
||||
let viewModel = WiredCameraTransferViewModel(
|
||||
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: ""),
|
||||
cameraService: MockWiredCameraService()
|
||||
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
|
||||
)
|
||||
viewModel.replacePhotos([
|
||||
makePendingPhoto(id: "today", capturedAt: today),
|
||||
@ -226,111 +95,8 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
XCTAssertNil(viewModel.errorMessage)
|
||||
}
|
||||
|
||||
/// 测试上传进度更新时保留 capturedAt,避免列表分组抖动。
|
||||
func testPipelineProgressUpdatePreservesCapturedAt() 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 originalCapturedAt = "2026-05-20 12:05:18"
|
||||
viewModel.replacePhotos([
|
||||
WiredTransferPhotoItem(
|
||||
id: "ptp_progress",
|
||||
fileName: "DSC_PROGRESS.JPG",
|
||||
thumbnailURL: "",
|
||||
previewURL: "",
|
||||
capturedAt: originalCapturedAt,
|
||||
fileSizeText: "2 MB",
|
||||
status: .uploading,
|
||||
progress: 10
|
||||
),
|
||||
])
|
||||
|
||||
let scope = downloadScope(account: account, albumID: context.albumId)
|
||||
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "DSC_PROGRESS.JPG", scope: scope)
|
||||
try Data(repeating: 0xAB, count: 16).write(to: fileURL)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
camera.downloadURL = fileURL
|
||||
camera.onNewAsset?(CameraAsset(id: "ptp_progress", filename: "DSC_PROGRESS.JPG", fileSize: 16))
|
||||
try await Task.sleep(nanoseconds: 300_000_000)
|
||||
|
||||
XCTAssertEqual(viewModel.photos.first?.capturedAt, originalCapturedAt)
|
||||
}
|
||||
|
||||
/// 测试较新拍摄时间的照片排在列表前面。
|
||||
func testNewerCapturedPhotoAppearsFirst() async throws {
|
||||
let camera = MockWiredCameraService()
|
||||
let context = WiredTransferContext(
|
||||
albumId: 6604,
|
||||
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 fileURL = FileManager.default.temporaryDirectory
|
||||
let olderURL = fileURL.appendingPathComponent("OLDER.JPG")
|
||||
let newerURL = fileURL.appendingPathComponent("NEWER.JPG")
|
||||
try Data(repeating: 0xAA, count: 8).write(to: olderURL)
|
||||
try Data(repeating: 0xBB, count: 8).write(to: newerURL)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: olderURL)
|
||||
try? FileManager.default.removeItem(at: newerURL)
|
||||
}
|
||||
|
||||
camera.downloadURL = olderURL
|
||||
camera.onNewAsset?(
|
||||
CameraAsset(
|
||||
id: "ptp_old",
|
||||
filename: "OLDER.JPG",
|
||||
fileSize: 8,
|
||||
creationDate: Date(timeIntervalSince1970: 1_700_000_000)
|
||||
)
|
||||
)
|
||||
try await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
camera.downloadURL = newerURL
|
||||
camera.onNewAsset?(
|
||||
CameraAsset(
|
||||
id: "ptp_new",
|
||||
filename: "NEWER.JPG",
|
||||
fileSize: 8,
|
||||
creationDate: Date(timeIntervalSince1970: 1_800_000_000)
|
||||
)
|
||||
)
|
||||
try await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
XCTAssertEqual(viewModel.photos.count, 2)
|
||||
XCTAssertEqual(viewModel.photos[0].id, "ptp_new")
|
||||
XCTAssertEqual(viewModel.photos[1].id, "ptp_old")
|
||||
}
|
||||
|
||||
/// 测试拍后传输模式下重传会从持久化记录恢复上传。
|
||||
func testRetryPhotoInPostShootModeUploadsPersistedFile() async throws {
|
||||
let camera = MockWiredCameraService()
|
||||
let context = WiredTransferContext(
|
||||
albumId: 1,
|
||||
albumName: "测试相册",
|
||||
@ -368,7 +134,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
)
|
||||
store.save(albumID: 1, records: [record])
|
||||
|
||||
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
|
||||
let viewModel = WiredCameraTransferViewModel(context: context)
|
||||
viewModel.transferModeOption = WiredCameraTransferViewModel.modePostShootTransfer
|
||||
|
||||
let api = MockTravelAlbumAPIForWiredTransfer()
|
||||
@ -418,8 +184,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
store.save(albumID: albumID, records: records)
|
||||
|
||||
let viewModel = WiredCameraTransferViewModel(
|
||||
context: WiredTransferContext(albumId: albumID, albumName: "批量上传", phone: "", orderNumber: ""),
|
||||
cameraService: MockWiredCameraService()
|
||||
context: WiredTransferContext(albumId: albumID, albumName: "批量上传", phone: "", orderNumber: "")
|
||||
)
|
||||
let oss = WiredTransferMockOSSUploadService()
|
||||
oss.uploadDelayNanoseconds = 150_000_000
|
||||
@ -447,32 +212,36 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
/// 测试切换传输模式后会按账号缓存选项。
|
||||
func testSelectTransferModePersistsOption() async {
|
||||
let account = makeTransferModeTestAccount()
|
||||
let context = WiredTransferContext(albumId: 7701, albumName: "模式缓存", phone: "", orderNumber: "")
|
||||
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: MockWiredCameraService())
|
||||
let api = MockTravelAlbumAPIForWiredTransfer()
|
||||
let oss = WiredTransferMockOSSUploadService()
|
||||
let store = makeTransferModeTestStore(account: account)
|
||||
let viewModel = WiredCameraTransferViewModel(
|
||||
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
|
||||
)
|
||||
await viewModel.start(
|
||||
api: MockTravelAlbumAPIForWiredTransfer(),
|
||||
ossService: WiredTransferMockOSSUploadService(),
|
||||
scenicID: 1,
|
||||
accountContext: account
|
||||
)
|
||||
|
||||
await viewModel.start(api: api, ossService: oss, scenicID: 1, accountContext: account)
|
||||
viewModel.selectTransferMode(
|
||||
WiredCameraTransferViewModel.modePostShootTransfer,
|
||||
api: api,
|
||||
ossService: oss,
|
||||
api: MockTravelAlbumAPIForWiredTransfer(),
|
||||
ossService: WiredTransferMockOSSUploadService(),
|
||||
scenicID: 1
|
||||
)
|
||||
|
||||
let store = makeTransferModeTestStore(account: account)
|
||||
XCTAssertEqual(store.loadTransferModeOption(), WiredCameraTransferViewModel.modePostShootTransfer)
|
||||
}
|
||||
|
||||
/// 测试再次进入有线传图页时会恢复上次选择的传输模式。
|
||||
/// 测试进入页面时会恢复缓存的传输模式。
|
||||
func testStartRestoresCachedTransferModeOption() async {
|
||||
let account = makeTransferModeTestAccount()
|
||||
let store = makeTransferModeTestStore(account: account)
|
||||
store.saveTransferModeOption(WiredCameraTransferViewModel.modePostShootTransfer)
|
||||
|
||||
let context = WiredTransferContext(albumId: 7702, albumName: "模式恢复", phone: "", orderNumber: "")
|
||||
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: MockWiredCameraService())
|
||||
|
||||
let viewModel = WiredCameraTransferViewModel(
|
||||
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
|
||||
)
|
||||
await viewModel.start(
|
||||
api: MockTravelAlbumAPIForWiredTransfer(),
|
||||
ossService: WiredTransferMockOSSUploadService(),
|
||||
@ -483,37 +252,6 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.transferModeOption, WiredCameraTransferViewModel.modePostShootTransfer)
|
||||
}
|
||||
|
||||
/// 测试下载完成后生成本地缩略图,并优先用于列表展示。
|
||||
func testDownloadedPhotoGeneratesThumbnailForList() async throws {
|
||||
let camera = MockWiredCameraService()
|
||||
let albumID = 6601
|
||||
let context = WiredTransferContext(albumId: albumID, albumName: "缩略图相册", phone: "", orderNumber: "")
|
||||
let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
|
||||
let account = AccountContext()
|
||||
account.applyLogin(profile: AccountProfile(userId: "thumb-user", displayName: "测试"))
|
||||
|
||||
await viewModel.start(
|
||||
api: MockTravelAlbumAPIForWiredTransfer(),
|
||||
ossService: WiredTransferMockOSSUploadService(),
|
||||
scenicID: 1,
|
||||
accountContext: account
|
||||
)
|
||||
|
||||
let scope = downloadScope(account: account, albumID: albumID)
|
||||
let imageURL = CameraDownloadStorage.uniqueLocalURL(for: "thumb_source.JPG", scope: scope)
|
||||
try makeJPEGData().write(to: imageURL)
|
||||
defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }
|
||||
|
||||
camera.downloadURL = imageURL
|
||||
camera.onNewAsset?(CameraAsset(id: "thumb_asset_001", filename: "thumb_source.JPG", fileSize: 128))
|
||||
try await Task.sleep(nanoseconds: 900_000_000)
|
||||
|
||||
let photo = try XCTUnwrap(viewModel.photos.first { $0.id == "thumb_asset_001" })
|
||||
XCTAssertTrue(photo.thumbnailURL.contains("/CameraDownloads/\(scope.accountDirectoryName)/\(albumID)/thumbnails/"))
|
||||
XCTAssertTrue(photo.previewURL.contains("/CameraDownloads/"))
|
||||
XCTAssertFalse(photo.previewURL.contains("/thumbnails/"))
|
||||
}
|
||||
|
||||
/// 测试旧记录没有缩略图时列表先占位,但仍保留原图预览能力。
|
||||
func testLegacyRecordWithoutThumbnailKeepsPreviewURL() async throws {
|
||||
let albumID = 6602
|
||||
@ -547,8 +285,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
store.save(albumID: albumID, records: [record])
|
||||
|
||||
let viewModel = WiredCameraTransferViewModel(
|
||||
context: WiredTransferContext(albumId: albumID, albumName: "旧记录", phone: "", orderNumber: ""),
|
||||
cameraService: MockWiredCameraService()
|
||||
context: WiredTransferContext(albumId: albumID, albumName: "旧记录", phone: "", orderNumber: "")
|
||||
)
|
||||
await viewModel.start(
|
||||
api: MockTravelAlbumAPIForWiredTransfer(),
|
||||
@ -578,10 +315,10 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
CameraDownloadStorage.removeScopeDirectory(for: otherScope)
|
||||
}
|
||||
|
||||
let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "clear_target.JPG", scope: scope)
|
||||
let otherFileURL = CameraDownloadStorage.uniqueLocalURL(for: "clear_other.JPG", scope: otherScope)
|
||||
try Data(repeating: 0x01, count: 8).write(to: fileURL)
|
||||
try Data(repeating: 0x02, count: 8).write(to: otherFileURL)
|
||||
let storeURL = CameraDownloadStorage.uniqueLocalURL(for: "store_photo.JPG", scope: scope)
|
||||
let otherURL = CameraDownloadStorage.uniqueLocalURL(for: "other_photo.JPG", scope: otherScope)
|
||||
try makeJPEGData().write(to: storeURL)
|
||||
try makeJPEGData().write(to: otherURL)
|
||||
|
||||
let store = WiredTransferPhotoStore(
|
||||
accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
|
||||
@ -589,40 +326,29 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
)
|
||||
store.save(albumID: albumID, records: [
|
||||
makePhotoRecord(
|
||||
id: "clear_target",
|
||||
fileName: "clear_target.JPG",
|
||||
localPath: CameraDownloadStorage.relativePath(for: fileURL),
|
||||
id: "store_photo",
|
||||
fileName: "store_photo.JPG",
|
||||
localPath: CameraDownloadStorage.relativePath(for: storeURL),
|
||||
albumID: albumID,
|
||||
userID: userID
|
||||
)
|
||||
])
|
||||
store.save(albumID: otherAlbumID, records: [
|
||||
makePhotoRecord(
|
||||
id: "clear_other",
|
||||
fileName: "clear_other.JPG",
|
||||
localPath: CameraDownloadStorage.relativePath(for: otherFileURL),
|
||||
albumID: otherAlbumID,
|
||||
userID: userID
|
||||
)
|
||||
])
|
||||
store.clearAlbum(albumID: albumID)
|
||||
|
||||
let deletedIDs = store.clearAlbum(albumID: albumID)
|
||||
|
||||
XCTAssertEqual(deletedIDs, Set(["clear_target"]))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: scopeDirectoryPath))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: otherFileURL.path))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: otherScopeDirectoryPath))
|
||||
}
|
||||
|
||||
/// 测试同一用户 ID 在不同账号类型下的有线传图记录与物理目录隔离。
|
||||
/// 测试同手机号不同账号类型的本地记录与物理目录隔离。
|
||||
func testSameUserIDDifferentAccountTypesAreIsolated() throws {
|
||||
let userID = "multi-account-user"
|
||||
let albumID = 8803
|
||||
let userID = "same-user-id"
|
||||
let albumID = 9901
|
||||
|
||||
let storeAccount = AccountContext()
|
||||
storeAccount.applyLogin(profile: AccountProfile(userId: userID, displayName: "门店"), accountType: "store")
|
||||
let photographerAccount = AccountContext()
|
||||
photographerAccount.applyLogin(profile: AccountProfile(userId: userID, displayName: "摄影师"), accountType: "photographer")
|
||||
|
||||
let storeScope = downloadScope(account: storeAccount, albumID: albumID)
|
||||
let photographerScope = downloadScope(account: photographerAccount, albumID: albumID)
|
||||
defer {
|
||||
@ -632,10 +358,10 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
|
||||
let storeURL = CameraDownloadStorage.uniqueLocalURL(for: "store_photo.JPG", scope: storeScope)
|
||||
let photographerURL = CameraDownloadStorage.uniqueLocalURL(for: "photographer_photo.JPG", scope: photographerScope)
|
||||
try Data(repeating: 0x03, count: 8).write(to: storeURL)
|
||||
try Data(repeating: 0x04, count: 8).write(to: photographerURL)
|
||||
try makeJPEGData().write(to: storeURL)
|
||||
try makeJPEGData().write(to: photographerURL)
|
||||
|
||||
let store = WiredTransferPhotoStore(
|
||||
let storeStore = WiredTransferPhotoStore(
|
||||
accountPrefixProvider: { storeAccount.accountCachePrefix ?? "guest_" },
|
||||
userIDProvider: { userID }
|
||||
)
|
||||
@ -643,7 +369,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
accountPrefixProvider: { photographerAccount.accountCachePrefix ?? "guest_" },
|
||||
userIDProvider: { userID }
|
||||
)
|
||||
store.save(albumID: albumID, records: [
|
||||
storeStore.save(albumID: albumID, records: [
|
||||
makePhotoRecord(
|
||||
id: "store_photo",
|
||||
fileName: "store_photo.JPG",
|
||||
@ -662,7 +388,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
)
|
||||
])
|
||||
|
||||
XCTAssertEqual(store.load(albumID: albumID).map(\.id), ["store_photo"])
|
||||
XCTAssertEqual(storeStore.load(albumID: albumID).map(\.id), ["store_photo"])
|
||||
XCTAssertEqual(photographerStore.load(albumID: albumID).map(\.id), ["photographer_photo"])
|
||||
XCTAssertNotEqual(
|
||||
CameraDownloadStorage.scopeDirectory(for: storeScope).path,
|
||||
@ -672,8 +398,7 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
|
||||
private func makeViewModelWithPendingPhotos() -> WiredCameraTransferViewModel {
|
||||
let viewModel = WiredCameraTransferViewModel(
|
||||
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: ""),
|
||||
cameraService: MockWiredCameraService()
|
||||
context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: "")
|
||||
)
|
||||
viewModel.replacePhotos([
|
||||
makePendingPhoto(id: "pending-1", capturedAt: "2026-05-20 12:05:18"),
|
||||
@ -741,63 +466,6 @@ final class WiredCameraTransferViewModelTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class DisconnectTrackingWiredCameraService: CameraServiceProtocol {
|
||||
let brand: CameraBrand = .sony
|
||||
var connectionState: CameraConnectionState = .disconnected
|
||||
var onConnectionStateChange: ((CameraConnectionState) -> Void)?
|
||||
var onNewAsset: ((CameraAsset) -> Void)?
|
||||
private(set) var connectCount = 0
|
||||
private(set) var detachCount = 0
|
||||
private(set) var shutdownCount = 0
|
||||
|
||||
func connect() async {
|
||||
connectCount += 1
|
||||
connectionState = .connected(deviceName: "Mock")
|
||||
onConnectionStateChange?(.connected(deviceName: "Mock"))
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
func detachFromUI() { detachCount += 1 }
|
||||
func shutdown() async { shutdownCount += 1 }
|
||||
|
||||
func listAssets() async throws -> [CameraAsset] { [] }
|
||||
|
||||
func downloadAsset(_ asset: CameraAsset) async throws -> URL {
|
||||
FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename)
|
||||
}
|
||||
}
|
||||
|
||||
@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 detachFromUI() {}
|
||||
func shutdown() 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] { [] }
|
||||
|
||||
Reference in New Issue
Block a user