184 lines
7.2 KiB
Swift
184 lines
7.2 KiB
Swift
//
|
|
// 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
|
|
}
|
|
}
|