Files
suixinkan_uikit/suixinkanTests/TravelAlbumOTGStorageTests.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
}
}