You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
101 lines
5.4 KiB
101 lines
5.4 KiB
import Foundation
|
|
import Testing
|
|
@testable import Music
|
|
|
|
// Verifies the save orchestration: DB always updated; file writeback best-effort;
|
|
// stats refreshed on success; warnings on unsupported format / writer failure.
|
|
struct TrackEditServiceTests {
|
|
|
|
// A spy writer we can make succeed or throw.
|
|
struct SpyWriter: TagWriter {
|
|
let shouldThrow: Bool
|
|
func write(_ fields: EditableTrackFields, to url: URL) throws {
|
|
if shouldThrow { throw TagWriterError.exportFailed }
|
|
// simulate a real write by appending a byte so size/mtime change.
|
|
let h = try FileHandle(forWritingTo: url); try h.seekToEnd()
|
|
try h.write(contentsOf: Data([0])); try h.close()
|
|
}
|
|
}
|
|
|
|
private func tempTrack(ext: String) throws -> Track {
|
|
let url = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent(UUID().uuidString + "." + ext)
|
|
try Data(repeating: 1, count: 100).write(to: url)
|
|
return .fixture(fileURL: url.absoluteString, fileFormat: ext)
|
|
}
|
|
|
|
@Test func supportedFormatSuccessUpdatesDBAndRefreshesStats() throws {
|
|
// Step 1: DB + a real temp file + an edit changing the title.
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t = try tempTrack(ext: "mp3"); try db.insert(&t)
|
|
let original = EditableTrackFields(from: t)
|
|
var edited = original; edited.title = "Edited"
|
|
// Step 2: save via a succeeding writer (injected).
|
|
let svc = TrackEditService(database: db, writerFactory: { _ in SpyWriter(shouldThrow: false) })
|
|
let warnings = svc.save(edited, editing: original.changedFields(to: edited), to: [t])
|
|
// Step 3: no warnings; DB has new title and refreshed hash (file changed).
|
|
#expect(warnings.isEmpty)
|
|
let f = try #require(db.fetchTracksByIds([t.id!]).first)
|
|
#expect(f.title == "Edited")
|
|
#expect(f.fileHash != t.fileHash)
|
|
try? FileManager.default.removeItem(at: URL(string: t.fileURL)!)
|
|
}
|
|
|
|
@Test func unsupportedFormatSavesDBOnlyWithWarning() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t = try tempTrack(ext: "flac"); try db.insert(&t)
|
|
var edited = EditableTrackFields(from: t); edited.album = "DB Only"
|
|
let svc = TrackEditService(database: db, writerFactory: TagWriterFactory.writer) // nil for flac
|
|
let warnings = svc.save(edited, editing: [.album], to: [t])
|
|
#expect(warnings.count == 1)
|
|
#expect(warnings.first?.kind == .dbOnlyUnsupported)
|
|
#expect(try #require(db.fetchTracksByIds([t.id!]).first).album == "DB Only")
|
|
try? FileManager.default.removeItem(at: URL(string: t.fileURL)!)
|
|
}
|
|
|
|
@Test func writerThrowsSavesDBOnlyWithFailureWarning() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t = try tempTrack(ext: "mp3"); try db.insert(&t)
|
|
var edited = EditableTrackFields(from: t); edited.genre = "Still Saved"
|
|
let svc = TrackEditService(database: db, writerFactory: { _ in SpyWriter(shouldThrow: true) })
|
|
let warnings = svc.save(edited, editing: [.genre], to: [t])
|
|
#expect(warnings.first?.kind == .fileWriteFailed)
|
|
#expect(try #require(db.fetchTracksByIds([t.id!]).first).genre == "Still Saved")
|
|
try? FileManager.default.removeItem(at: URL(string: t.fileURL)!)
|
|
}
|
|
|
|
@Test func dateAddedOnlyEditIsDBOnlyNoFileWrite() throws {
|
|
// Step 1: insert an mp3 track and remember its file hash.
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t = try tempTrack(ext: "mp3"); try db.insert(&t)
|
|
let originalHash = t.fileHash
|
|
// Step 2: edit ONLY dateAdded, with a writer that THROWS if ever called —
|
|
// proving dateAdded is DB-only and triggers no file write.
|
|
var edited = EditableTrackFields(from: t)
|
|
let newDate = Date(timeIntervalSince1970: 1_000_000)
|
|
edited.dateAdded = newDate
|
|
let svc = TrackEditService(database: db, writerFactory: { _ in SpyWriter(shouldThrow: true) })
|
|
let warnings = svc.save(edited, editing: [.dateAdded], to: [t])
|
|
// Step 3: no warnings (no write attempted); DB has the new date; file untouched.
|
|
#expect(warnings.isEmpty)
|
|
let f = try #require(db.fetchTracksByIds([t.id!]).first)
|
|
#expect(f.dateAdded == newDate)
|
|
#expect(f.fileHash == originalHash)
|
|
try? FileManager.default.removeItem(at: URL(string: t.fileURL)!)
|
|
}
|
|
|
|
@Test func multiTrackAppliesOnlyEditedFields() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
var a = try tempTrack(ext: "flac"); a.album = "OldA"; a.genre = "RockA"; try db.insert(&a)
|
|
var b = try tempTrack(ext: "flac"); b.album = "OldB"; b.genre = "RockB"; try db.insert(&b)
|
|
var edited = EditableTrackFields(from: a); edited.album = "Shared"
|
|
let svc = TrackEditService(database: db, writerFactory: TagWriterFactory.writer)
|
|
_ = svc.save(edited, editing: [.album], to: [a, b])
|
|
// album applied to both; each genre untouched.
|
|
#expect(try #require(db.fetchTracksByIds([a.id!]).first).album == "Shared")
|
|
#expect(try #require(db.fetchTracksByIds([b.id!]).first).album == "Shared")
|
|
#expect(try #require(db.fetchTracksByIds([b.id!]).first).genre == "RockB")
|
|
try? FileManager.default.removeItem(at: URL(string: a.fileURL)!)
|
|
try? FileManager.default.removeItem(at: URL(string: b.fileURL)!)
|
|
}
|
|
}
|
|
|