velody/packages/apple/VelodyPersistence/Tests/VelodyPersistenceTests/LocalCatalogServiceTests.swift
2026-05-28 13:32:40 +02:00

274 lines
9.7 KiB
Swift

import Foundation
import XCTest
@testable import VelodyPersistence
import VelodyDomain
final class LocalCatalogServiceTests: XCTestCase {
func testScanPersistsTracksAndSkipsDuplicateSHA() async throws {
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
let service = DefaultLocalCatalogService(repository: repository)
let modifiedAt = Date(timeIntervalSince1970: 1_000)
let result = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Alpha",
path: "/Music/Alpha.mp3",
sha256: "sha-alpha",
modifiedAt: modifiedAt
),
makeScannedTrack(
title: "Alpha Copy",
path: "/Music/Copies/Alpha.mp3",
sha256: "sha-alpha",
modifiedAt: modifiedAt
),
],
in: URL(fileURLWithPath: "/Music"),
scannedAt: Date(timeIntervalSince1970: 1_100)
)
XCTAssertEqual(result.insertedTrackCount, 1)
XCTAssertEqual(result.skippedDuplicateTrackCount, 1)
XCTAssertEqual(result.deletedTrackCount, 0)
XCTAssertEqual(result.tracks.count, 1)
XCTAssertEqual(result.tracks.first?.localFilePath, "/Music/Alpha.mp3")
let storedTracks = try await repository.loadLocalTracks(
origin: .localScan,
includeDeleted: true
)
XCTAssertEqual(storedTracks.count, 1)
XCTAssertEqual(storedTracks.first?.sha256, "sha-alpha")
XCTAssertEqual(storedTracks.first?.fileModifiedAt, modifiedAt)
}
func testModifiedFileAtSamePathUpdatesExistingTrackInsteadOfDuplicating() async throws {
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
let service = DefaultLocalCatalogService(repository: repository)
let folderURL = URL(fileURLWithPath: "/Music")
_ = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Original",
path: "/Music/Track.mp3",
sha256: "sha-original",
modifiedAt: Date(timeIntervalSince1970: 2_000)
),
],
in: folderURL,
scannedAt: Date(timeIntervalSince1970: 2_100)
)
let result = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Updated Title",
path: "/Music/Track.mp3",
sha256: "sha-updated",
modifiedAt: Date(timeIntervalSince1970: 2_200)
),
],
in: folderURL,
scannedAt: Date(timeIntervalSince1970: 2_300)
)
XCTAssertEqual(result.insertedTrackCount, 0)
XCTAssertEqual(result.updatedTrackCount, 1)
XCTAssertEqual(result.deletedTrackCount, 0)
XCTAssertEqual(result.tracks.count, 1)
XCTAssertEqual(result.tracks.first?.title, "Updated Title")
XCTAssertEqual(result.tracks.first?.sha256, "sha-updated")
let storedTracks = try await repository.loadLocalTracks(
origin: .localScan,
includeDeleted: true
)
XCTAssertEqual(storedTracks.count, 1)
XCTAssertEqual(storedTracks.first?.title, "Updated Title")
XCTAssertEqual(storedTracks.first?.sha256, "sha-updated")
}
func testRescanMarksDeletedTracksAndReactivatesExistingSHA() async throws {
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
let service = DefaultLocalCatalogService(repository: repository)
let folderURL = URL(fileURLWithPath: "/Music")
_ = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Alpha",
path: "/Music/Alpha.mp3",
sha256: "sha-alpha",
modifiedAt: Date(timeIntervalSince1970: 3_000)
),
makeScannedTrack(
title: "Beta",
path: "/Music/Beta.mp3",
sha256: "sha-beta",
modifiedAt: Date(timeIntervalSince1970: 3_000)
),
],
in: folderURL,
scannedAt: Date(timeIntervalSince1970: 3_100)
)
let deleteResult = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Alpha",
path: "/Music/Alpha.mp3",
sha256: "sha-alpha",
modifiedAt: Date(timeIntervalSince1970: 3_200)
),
],
in: folderURL,
scannedAt: Date(timeIntervalSince1970: 3_300)
)
XCTAssertEqual(deleteResult.deletedTrackCount, 1)
XCTAssertEqual(deleteResult.tracks.count, 1)
let reactivationResult = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Alpha",
path: "/Music/Alpha.mp3",
sha256: "sha-alpha",
modifiedAt: Date(timeIntervalSince1970: 3_400)
),
makeScannedTrack(
title: "Beta",
path: "/Music/Recovered/Beta.mp3",
sha256: "sha-beta",
modifiedAt: Date(timeIntervalSince1970: 3_400)
),
],
in: folderURL,
scannedAt: Date(timeIntervalSince1970: 3_500)
)
XCTAssertEqual(reactivationResult.reactivatedTrackCount, 1)
XCTAssertEqual(reactivationResult.deletedTrackCount, 0)
XCTAssertEqual(reactivationResult.tracks.count, 2)
let storedTracks = try await repository.loadLocalTracks(
origin: .localScan,
includeDeleted: true
)
XCTAssertEqual(storedTracks.count, 2)
XCTAssertEqual(
storedTracks.first(where: { $0.sha256 == "sha-beta" })?.localFilePath,
"/Music/Recovered/Beta.mp3"
)
XCTAssertEqual(
storedTracks.first(where: { $0.sha256 == "sha-beta" })?.isDeleted,
false
)
}
func testRepositoryPersistsTracksAcrossInstances() async throws {
let fileManager = FileManager.default
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
UUID().uuidString,
isDirectory: true
)
let databaseURL = tempDirectory.appendingPathComponent("catalog.store")
let folderURL = URL(fileURLWithPath: "/Music")
defer {
try? fileManager.removeItem(at: tempDirectory)
}
let firstRepository = try SwiftDataTrackRepository(databaseURL: databaseURL)
let firstService = DefaultLocalCatalogService(repository: firstRepository)
_ = try await firstService.reconcileScanResults(
[
makeScannedTrack(
title: "Persisted",
path: "/Music/Persisted.mp3",
sha256: "sha-persisted",
modifiedAt: Date(timeIntervalSince1970: 4_000)
),
],
in: folderURL,
scannedAt: Date(timeIntervalSince1970: 4_100)
)
let secondRepository = try SwiftDataTrackRepository(databaseURL: databaseURL)
let storedTracks = try await secondRepository.loadLocalTracks(
origin: .localScan,
includeDeleted: false
)
XCTAssertEqual(storedTracks.count, 1)
XCTAssertEqual(storedTracks.first?.title, "Persisted")
XCTAssertEqual(storedTracks.first?.sha256, "sha-persisted")
}
func testSyncBootstrapReplacementLeavesLocalCatalogIntact() async throws {
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
let service = DefaultLocalCatalogService(repository: repository)
_ = try await service.reconcileScanResults(
[
makeScannedTrack(
title: "Local Track",
path: "/Music/Local.mp3",
sha256: "sha-local",
modifiedAt: Date(timeIntervalSince1970: 5_000)
),
],
in: URL(fileURLWithPath: "/Music"),
scannedAt: Date(timeIntervalSince1970: 5_100)
)
try await repository.replaceTracks(
[
LibraryTrack(
id: "remote-track-1",
title: "Remote Track",
artist: "Remote Artist",
album: "Remote Album",
durationSeconds: 180,
localFilePath: "",
sha256: nil
),
]
)
let localTracks = try await repository.loadLocalTracks(
origin: .localScan,
includeDeleted: false
)
let remoteTracks = try await repository.loadLocalTracks(
origin: .syncBootstrap,
includeDeleted: false
)
let allTracks = try await repository.loadTracks()
XCTAssertEqual(localTracks.count, 1)
XCTAssertEqual(remoteTracks.count, 1)
XCTAssertEqual(allTracks.count, 2)
}
private func makeScannedTrack(
title: String,
path: String,
sha256: String,
modifiedAt: Date
) -> ScannedLocalTrack {
ScannedLocalTrack(
title: title,
artist: "Artist",
album: "Album",
durationSeconds: 180,
localFilePath: path,
sha256: sha256,
fileModifiedAt: modifiedAt
)
}
}