274 lines
9.7 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|