// // TravelAlbumOTGStorageTests.swift // suixinkanTests // import XCTest @testable import suixinkan /// 旅拍相册 OTG 本地存储测试。 final class TravelAlbumOTGStorageTests: XCTestCase { func testStoragePathContainsAccountScenicStoreAndAlbum() throws { let root = try makeTempDirectory() defer { try? FileManager.default.removeItem(at: root) } let store = makeStore(root: root) let originals = try store.originalsDirectory(albumId: 33) let previews = try store.previewsDirectory(albumId: 33) XCTAssertTrue(originals.path.contains("u1_photog/scenic_11/store_22/album_33/originals")) XCTAssertTrue(previews.path.contains("u1_photog/scenic_11/store_22/album_33/previews")) } func testUniqueOriginalFileURLAvoidsOverwrite() throws { let root = try makeTempDirectory() defer { try? FileManager.default.removeItem(at: root) } let store = makeStore(root: root) let first = try store.uniqueOriginalFileURL(filename: "IMG/1.JPG", albumId: 33) try Data([1]).write(to: first) let second = try store.uniqueOriginalFileURL(filename: "IMG/1.JPG", albumId: 33) XCTAssertEqual(first.lastPathComponent, "IMG_1.JPG") XCTAssertEqual(second.lastPathComponent, "IMG_1_1.JPG") } func testLoadNormalizesInterruptedTransferToPending() throws { let root = try makeTempDirectory() defer { try? FileManager.default.removeItem(at: root) } let store = makeStore(root: root) let file = try store.uniqueOriginalFileURL(filename: "A.JPG", albumId: 33) try Data([1, 2, 3]).write(to: file) let record = TravelAlbumOTGPhotoRecord( id: "1", fileName: "A.JPG", localPath: file.path, capturedAt: "2026-07-07 10:00:00", fileSizeBytes: 3, status: .uploading, progress: 50, albumId: 33, userId: "u1" ) store.save([record], albumId: 33) let loaded = store.load(albumId: 33) XCTAssertEqual(loaded.first?.status, .pending) XCTAssertEqual(loaded.first?.progress, 0) XCTAssertEqual(loaded.first?.localPath, "originals/A.JPG") XCTAssertEqual(store.absoluteURL(for: loaded.first?.localPath ?? "", albumId: 33), file) } func testRemoveAndClearAlbumDeleteFiles() throws { let root = try makeTempDirectory() defer { try? FileManager.default.removeItem(at: root) } let store = makeStore(root: root) let file = try store.uniqueOriginalFileURL(filename: "A.JPG", albumId: 33) try Data([1]).write(to: file) let record = TravelAlbumOTGPhotoRecord( id: "1", fileName: "A.JPG", localPath: file.path, capturedAt: "2026-07-07 10:00:00", fileSizeBytes: 1, status: .pending, albumId: 33, userId: "u1" ) store.save([record], albumId: 33) store.remove(albumId: 33, photoId: "1") XCTAssertFalse(FileManager.default.fileExists(atPath: file.path)) XCTAssertTrue(store.load(albumId: 33).isEmpty) let another = try store.uniqueOriginalFileURL(filename: "B.JPG", albumId: 33) try Data([2]).write(to: another) store.clearAlbum(albumId: 33) XCTAssertFalse(FileManager.default.fileExists(atPath: another.deletingLastPathComponent().deletingLastPathComponent().path)) } func testLegacyRecordGetsStableClientPhotoIdOnLoad() throws { let root = try makeTempDirectory() defer { try? FileManager.default.removeItem(at: root) } let store = makeStore(root: root) let legacyJSON = """ {"id":"legacy","sourceId":"legacy","fileName":"A.JPG","localPath":"",\ "thumbnailPath":"","capturedAt":"2026-07-16 12:00:00","fileSizeBytes":1,\ "status":"UPLOADED","progress":100,"albumId":33,"userId":"u1",\ "remoteUrl":"https://cdn/a.jpg","updatedAt":1} """.data(using: .utf8)! let legacy = try JSONDecoder().decode(TravelAlbumOTGPhotoRecord.self, from: legacyJSON) XCTAssertTrue(legacy.clientPhotoId.isEmpty) store.save([legacy], albumId: 33) let first = try XCTUnwrap(store.load(albumId: 33).first?.clientPhotoId) let second = try XCTUnwrap(store.load(albumId: 33).first?.clientPhotoId) XCTAssertNotNil(UUID(uuidString: first)) XCTAssertEqual(first, first.lowercased()) XCTAssertEqual(first, second) } func testServerStatusPolicyPromotesDemotesAndPreservesActiveOrChangedRecords() { let matched = makeStatusRecord(id: "matched", clientPhotoId: "server-id", status: .failed) let missing = makeStatusRecord(id: "missing", clientPhotoId: "missing-id", status: .uploaded) let active = makeStatusRecord(id: "active", clientPhotoId: "active-id", status: .uploaded) let changedBaseline = makeStatusRecord(id: "changed", clientPhotoId: "changed-id", status: .uploaded) var changed = changedBaseline changed.progress = 37 let result = TravelAlbumOTGServerStatusPolicy.reconcile( records: [matched, missing, active, changed], baselineRecordsById: [ "matched": matched, "missing": missing, "active": active, "changed": changedBaseline, ], activePhotoIds: ["active"], serverClientPhotoIds: [" SERVER-ID "], now: 9 ) let byId = Dictionary(uniqueKeysWithValues: result.map { ($0.id, $0) }) XCTAssertEqual(byId["matched"]?.status, .uploaded) XCTAssertEqual(byId["matched"]?.progress, 100) XCTAssertEqual(byId["missing"]?.status, .pending) XCTAssertEqual(byId["active"]?.status, .uploaded) XCTAssertEqual(byId["changed"]?.status, .uploaded) } private func makeStatusRecord( id: String, clientPhotoId: String, status: TravelAlbumOTGUploadStatus ) -> TravelAlbumOTGPhotoRecord { TravelAlbumOTGPhotoRecord( id: id, clientPhotoId: clientPhotoId, fileName: "\(id).JPG", localPath: "", capturedAt: "2026-07-16 12:00:00", fileSizeBytes: 1, status: status, progress: status == .uploaded ? 100 : 0, albumId: 33, userId: "u1", remoteUrl: "https://cdn/\(id).jpg", updatedAt: 1 ) } private func makeStore(root: URL) -> TravelAlbumOTGPhotoStore { TravelAlbumOTGPhotoStore( context: TravelAlbumOTGStorageContext( accountCachePrefix: "u1_photog", userId: "u1", scenicId: 11, storeId: 22 ), applicationSupportDirectory: root.appendingPathComponent("ApplicationSupport", isDirectory: true), cachesDirectory: root.appendingPathComponent("Caches", isDirectory: true) ) } private func makeTempDirectory() throws -> URL { let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url } }