| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import SwiftData
- import XCTest
- @testable import MixBoard
- @MainActor
- final class SyncManagerTests: XCTestCase {
- private var container: ModelContainer!
- private var context: ModelContext!
- private var syncManager: SyncManager!
- override func setUp() async throws {
- let config = ModelConfiguration(isStoredInMemoryOnly: true)
- container = try ModelContainer(
- for: Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self,
- configurations: config
- )
- context = ModelContext(container)
- syncManager = SyncManager()
- }
- override func tearDown() {
- // Clean up any exported sync file
- try? FileManager.default.removeItem(at: SyncManager.syncFileURL)
- }
- // MARK: - Sync File Paths
- func testSyncDirectoryExists() {
- let dir = SyncManager.syncDirectory
- var isDir: ObjCBool = false
- XCTAssertTrue(FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDir))
- XCTAssertTrue(isDir.boolValue)
- }
- func testSyncFileURLHasCorrectFilename() {
- let url = SyncManager.syncFileURL
- XCTAssertEqual(url.lastPathComponent, "mixboard-playlists.json")
- }
- // MARK: - Export
- func testExportEmptyPlaylists() {
- syncManager.exportPlaylists([])
- XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
- XCTAssertNotNil(syncManager.lastSyncDate)
- XCTAssertFalse(syncManager.isSyncing)
- }
- func testExportSinglePlaylist() {
- let playlist = Playlist(name: "Friday Mix", notes: "Great set", color: "#FF0000")
- playlist.targetBPM = 128.0
- let track = Track(title: "Anthem", artist: "DJ Test", filePath: "Music/anthem.mp3", fileName: "anthem.mp3", duration: 240)
- track.bpm = 126.5
- track.musicalKey = "Am"
- playlist.addTrack(track, crossfadeDuration: 3.0)
- syncManager.exportPlaylists([playlist])
- XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
- XCTAssertNil(syncManager.syncError)
- }
- func testExportMultiplePlaylists() {
- let p1 = Playlist(name: "Mix 1")
- let p2 = Playlist(name: "Mix 2")
- p1.addTrack(Track(title: "Song A", filePath: "Music/a.mp3", fileName: "a.mp3"))
- p2.addTrack(Track(title: "Song B", filePath: "Music/b.mp3", fileName: "b.mp3"))
- syncManager.exportPlaylists([p1, p2])
- XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
- XCTAssertNil(syncManager.syncError)
- }
- // MARK: - Import
- func testImportFromExportedFile() throws {
- let playlist = Playlist(name: "Round Trip")
- let track = Track(title: "Test", artist: "Artist", filePath: "Music/test.mp3", fileName: "test.mp3", duration: 180)
- playlist.addTrack(track)
- syncManager.exportPlaylists([playlist])
- let imported = try syncManager.importPlaylists(from: SyncManager.syncFileURL, context: context)
- XCTAssertEqual(imported.count, 1)
- XCTAssertEqual(imported[0].name, "Round Trip")
- XCTAssertEqual(imported[0].entries.count, 1)
- XCTAssertEqual(imported[0].entries[0].filename, "test.mp3")
- }
- func testImportInvalidJSON() {
- let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("invalid.json")
- try? "not json at all".data(using: .utf8)?.write(to: tempURL)
- defer { try? FileManager.default.removeItem(at: tempURL) }
- XCTAssertThrowsError(try syncManager.importPlaylists(from: tempURL, context: context))
- }
- func testImportNonExistentFile() {
- let fakeURL = URL(fileURLWithPath: "/nonexistent/fake.json")
- XCTAssertThrowsError(try syncManager.importPlaylists(from: fakeURL, context: context))
- }
- // MARK: - Merge
- func testMergeWithMatchingTracks() throws {
- // Insert a track into the DB
- let track = Track(title: "Matched", artist: "DJ", filePath: "Music/matched.mp3", fileName: "matched.mp3", duration: 200)
- context.insert(track)
- try context.save()
- // Create sync data referencing the same filename
- let syncEntry = SyncEntry(from: PlaylistEntry(position: 0, track: track))
- let syncPlaylist = SyncPlaylist(
- from: {
- let pl = Playlist(name: "Imported Mix")
- pl.addTrack(track)
- return pl
- }()
- )
- let result = syncManager.mergeImportedPlaylists([syncPlaylist], existingTracks: [track], context: context)
- XCTAssertEqual(result.created, 1)
- XCTAssertEqual(result.matched, 1)
- XCTAssertEqual(result.unmatched, 0)
- }
- func testMergeWithUnmatchedTracks() {
- // No tracks in the DB, so all entries will be unmatched
- let playlist = Playlist(name: "Unmatched")
- let fakeTrack = Track(title: "Missing", filePath: "Music/missing.mp3", fileName: "missing.mp3")
- playlist.addTrack(fakeTrack)
- let syncPlaylist = SyncPlaylist(from: playlist)
- let result = syncManager.mergeImportedPlaylists([syncPlaylist], existingTracks: [], context: context)
- XCTAssertEqual(result.created, 1)
- XCTAssertEqual(result.matched, 0)
- XCTAssertEqual(result.unmatched, 1)
- }
- func testMergeCreatesCorrectNumberOfPlaylists() {
- let p1 = Playlist(name: "Mix A")
- let p2 = Playlist(name: "Mix B")
- let p3 = Playlist(name: "Mix C")
- let syncs = [SyncPlaylist(from: p1), SyncPlaylist(from: p2), SyncPlaylist(from: p3)]
- let result = syncManager.mergeImportedPlaylists(syncs, existingTracks: [], context: context)
- XCTAssertEqual(result.created, 3)
- }
- func testMergePreservesCrossfadeSettings() throws {
- let track = Track(title: "Song", filePath: "Music/song.mp3", fileName: "song.mp3", duration: 300)
- context.insert(track)
- try context.save()
- let playlist = Playlist(name: "Mix")
- playlist.addTrack(track, crossfadeDuration: 5.0)
- // Modify the entry offsets
- let entry = playlist.sortedEntries.first!
- entry.startOffset = 10
- entry.endOffset = 280
- entry.gainAdjustment = 3.5
- let syncPlaylist = SyncPlaylist(from: playlist)
- let result = syncManager.mergeImportedPlaylists([syncPlaylist], existingTracks: [track], context: context)
- XCTAssertEqual(result.matched, 1)
- // Verify the merged playlist has the correct settings
- let descriptor = FetchDescriptor<Playlist>(predicate: #Predicate { $0.name == "Mix" })
- let fetched = try context.fetch(descriptor)
- XCTAssertEqual(fetched.count, 1)
- let mergedEntry = fetched.first?.sortedEntries.first
- XCTAssertEqual(mergedEntry?.crossfadeDuration, 5.0)
- XCTAssertEqual(mergedEntry?.startOffset, 10)
- XCTAssertEqual(mergedEntry?.endOffset, 280)
- XCTAssertEqual(mergedEntry?.gainAdjustment, 3.5)
- }
- // MARK: - Round-Trip Export → Import → Merge
- func testFullRoundTrip() throws {
- // Create and export
- let track1 = Track(title: "Alpha", artist: "DJ One", filePath: "Music/alpha.mp3", fileName: "alpha.mp3", duration: 200)
- track1.bpm = 128
- track1.musicalKey = "Cm"
- let track2 = Track(title: "Beta", artist: "DJ Two", filePath: "Music/beta.flac", fileName: "beta.flac", duration: 180)
- let playlist = Playlist(name: "Export Test", notes: "Testing sync", color: "#00FF00")
- playlist.targetBPM = 130
- playlist.addTrack(track1, crossfadeDuration: 3.0)
- playlist.addTrack(track2, crossfadeDuration: 2.5)
- syncManager.exportPlaylists([playlist])
- XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
- // Import back
- let imported = try syncManager.importPlaylists(from: SyncManager.syncFileURL, context: context)
- XCTAssertEqual(imported.count, 1)
- let sp = imported[0]
- XCTAssertEqual(sp.name, "Export Test")
- XCTAssertEqual(sp.notes, "Testing sync")
- XCTAssertEqual(sp.color, "#00FF00")
- XCTAssertEqual(sp.targetBPM, 130)
- XCTAssertEqual(sp.entries.count, 2)
- XCTAssertEqual(sp.entries[0].filename, "alpha.mp3")
- XCTAssertEqual(sp.entries[0].bpm, 128)
- XCTAssertEqual(sp.entries[0].musicalKey, "Cm")
- XCTAssertEqual(sp.entries[0].crossfadeDuration, 3.0)
- XCTAssertEqual(sp.entries[1].filename, "beta.flac")
- XCTAssertEqual(sp.entries[1].crossfadeDuration, 2.5)
- // Merge with matching tracks in DB
- context.insert(track1)
- context.insert(track2)
- try context.save()
- let result = syncManager.mergeImportedPlaylists(imported, existingTracks: [track1, track2], context: context)
- XCTAssertEqual(result.created, 1)
- XCTAssertEqual(result.matched, 2)
- XCTAssertEqual(result.unmatched, 0)
- }
- // MARK: - SyncPayload Encoding
- func testSyncPayloadVersionField() throws {
- syncManager.exportPlaylists([])
- let data = try Data(contentsOf: SyncManager.syncFileURL)
- let decoder = JSONDecoder()
- decoder.dateDecodingStrategy = .iso8601
- let payload = try decoder.decode(SyncPayload.self, from: data)
- XCTAssertEqual(payload.version, 1)
- }
- func testSyncPayloadExportedFromField() throws {
- syncManager.exportPlaylists([])
- let data = try Data(contentsOf: SyncManager.syncFileURL)
- let decoder = JSONDecoder()
- decoder.dateDecodingStrategy = .iso8601
- let payload = try decoder.decode(SyncPayload.self, from: data)
- XCTAssertFalse(payload.exportedFrom.isEmpty)
- }
- // MARK: - State
- func testInitialState() {
- XCTAssertNil(syncManager.lastSyncDate)
- XCTAssertFalse(syncManager.isSyncing)
- XCTAssertNil(syncManager.syncError)
- }
- func testExportUpdatesLastSyncDate() {
- XCTAssertNil(syncManager.lastSyncDate)
- syncManager.exportPlaylists([])
- XCTAssertNotNil(syncManager.lastSyncDate)
- }
- }
|