import Foundation import Testing @testable import MixBoardiOS // MARK: - ChadMusic Model Tests @Suite("ChadMusic Models") struct ChadMusicModelTests { @Test("ChadCategory decodes correctly") func decodeChadCategory() throws { let json = """ {"item": "Rock", "count": 42} """.data(using: .utf8)! let category = try JSONDecoder().decode(ChadCategory.self, from: json) #expect(category.item == "Rock") #expect(category.count == 42) #expect(category.id == "Rock") #expect(category.name == "Rock") } @Test("ChadCategory decodes with null count") func decodeChadCategoryNullCount() throws { let json = """ {"item": "Jazz", "count": null} """.data(using: .utf8)! let category = try JSONDecoder().decode(ChadCategory.self, from: json) #expect(category.item == "Jazz") #expect(category.count == nil) } @Test("ChadAlbum decodes with snake_case keys") func decodeChadAlbum() throws { let json = """ { "id": "abc123", "album": "Dark Side of the Moon", "artist": "Pink Floyd", "year": 1973, "genre": "Progressive Rock", "track_count": 10, "cover": "/covers/abc123.jpg", "publisher": "Harvest", "country": "UK", "type": "Album", "status": "Official", "total_duration": 2580.0, "original_date": "1973-03-01", "mb_id": "some-uuid" } """.data(using: .utf8)! let album = try JSONDecoder().decode(ChadAlbum.self, from: json) #expect(album.id == "abc123") #expect(album.title == "Dark Side of the Moon") #expect(album.artist == "Pink Floyd") #expect(album.year == 1973) #expect(album.genre == "Progressive Rock") #expect(album.trackCount == 10) #expect(album.totalDuration == 2580.0) } @Test("ChadAlbum title defaults to Untitled when album is null") func chadAlbumUntitled() throws { let json = """ {"id": "x", "album": null, "artist": null, "year": null, "genre": null, "track_count": null, "cover": null, "publisher": null, "country": null, "type": null, "status": null, "total_duration": null, "original_date": null, "mb_id": null} """.data(using: .utf8)! let album = try JSONDecoder().decode(ChadAlbum.self, from: json) #expect(album.title == "Untitled") } @Test("ChadTrack decodes with all fields") func decodeChadTrack() throws { let json = """ { "id": "track1", "title": "Breathe", "artist": "Pink Floyd", "album_artist": "Pink Floyd", "album": "Dark Side of the Moon", "duration": 169.0, "no": 2, "url": "/music/Pink Floyd/DSOTM/02-Breathe.flac", "bit_rate": 1411, "year": 1973, "cover": "/covers/abc.jpg" } """.data(using: .utf8)! let track = try JSONDecoder().decode(ChadTrack.self, from: json) #expect(track.id == "track1") #expect(track.title == "Breathe") #expect(track.artist == "Pink Floyd") #expect(track.albumArtist == "Pink Floyd") #expect(track.album == "Dark Side of the Moon") #expect(track.duration == 169.0) #expect(track.trackNumber == 2) #expect(track.url == "/music/Pink Floyd/DSOTM/02-Breathe.flac") #expect(track.bitRate == 1411) #expect(track.year == 1973) } @Test("ChadTrack formatted duration") func trackFormattedDuration() throws { let json = """ {"id": "t", "title": "T", "artist": null, "album_artist": null, "album": null, "duration": 185.0, "no": null, "url": "/x", "bit_rate": null, "year": null, "cover": null} """.data(using: .utf8)! let track = try JSONDecoder().decode(ChadTrack.self, from: json) #expect(track.formattedDuration == "3:05") } @Test("ChadTrack formatted duration with nil shows dash") func trackFormattedDurationNil() throws { let json = """ {"id": "t", "title": "T", "artist": null, "album_artist": null, "album": null, "duration": null, "no": null, "url": "/x", "bit_rate": null, "year": null, "cover": null} """.data(using: .utf8)! let track = try JSONDecoder().decode(ChadTrack.self, from: json) #expect(track.formattedDuration == "—") } @Test("ChadStats decodes") func decodeChadStats() throws { let json = """ {"tracks": 5000, "albums": 400, "artists": 200, "duration": "3d 14h 22m"} """.data(using: .utf8)! let stats = try JSONDecoder().decode(ChadStats.self, from: json) #expect(stats.tracks == 5000) #expect(stats.albums == 400) #expect(stats.artists == 200) #expect(stats.duration == "3d 14h 22m") } @Test("ChadCategoryType has correct display names") func categoryDisplayNames() { #expect(ChadCategoryType.album.displayName == "Albums") #expect(ChadCategoryType.artist.displayName == "Artists") #expect(ChadCategoryType.genre.displayName == "Genres") #expect(ChadCategoryType.year.displayName == "Years") } @Test("ChadCategoryType has icons") func categoryIcons() { for category in ChadCategoryType.allCases { #expect(!category.icon.isEmpty) } } } // MARK: - Track.fromCloud Tests @Suite("Track Cloud Factory") struct TrackFromCloudTests { @Test("fromCloud creates track with correct fields") func fromCloudBasic() { let chadTrack = ChadTrack( id: "abc123", title: "Test Track", artist: "Test Artist", albumArtist: nil, album: "Test Album", duration: 240.0, no: 3, url: "/music/Test/Album/03-Track.flac", bitRate: 1411, year: 2024, cover: nil ) let track = Track.fromCloud(chadTrack) #expect(track.title == "Test Track") #expect(track.artist == "Test Artist") #expect(track.album == "Test Album") #expect(track.duration == 240.0) #expect(track.isCloud == true) #expect(track.cloudStreamPath == "/music/Test/Album/03-Track.flac") #expect(track.cloudTrackId == "abc123") #expect(track.year == 2024) #expect(track.filePath == "") } @Test("fromCloud handles nil optional fields") func fromCloudNilFields() { let chadTrack = ChadTrack( id: "x", title: "Minimal", artist: nil, albumArtist: nil, album: nil, duration: nil, no: nil, url: "/music/file.mp3", bitRate: nil, year: nil, cover: nil ) let track = Track.fromCloud(chadTrack) #expect(track.title == "Minimal") #expect(track.artist == "") #expect(track.album == "") #expect(track.duration == 0) #expect(track.isCloud == true) #expect(track.year == nil) } @Test("hasLocalFile is false for cloud tracks") func cloudTrackHasNoLocalFile() { let chadTrack = ChadTrack( id: "x", title: "Cloud", artist: nil, albumArtist: nil, album: nil, duration: 100, no: nil, url: "/music/file.mp3", bitRate: nil, year: nil, cover: nil ) let track = Track.fromCloud(chadTrack) #expect(track.hasLocalFile == false) } } // MARK: - ChadMusicAPIClient Tests @Suite("ChadMusicAPIClient") struct ChadMusicAPIClientTests { @Test("streamURL builds correct URL") @MainActor func streamURLBuilding() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com" let url = client.streamURL(for: "/music/Artist/Album/track.flac") #expect(url?.absoluteString == "https://music.example.com/music/Artist/Album/track.flac") } @Test("streamURL handles trailing slash on server") @MainActor func streamURLTrailingSlash() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com/" let url = client.streamURL(for: "/music/file.mp3") #expect(url?.absoluteString == "https://music.example.com/music/file.mp3") } @Test("streamURL handles path without leading slash") @MainActor func streamURLNoLeadingSlash() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com" let url = client.streamURL(for: "music/file.mp3") #expect(url?.absoluteString == "https://music.example.com/music/file.mp3") } @Test("streamURL returns nil for empty server") @MainActor func streamURLEmptyServer() { let client = ChadMusicAPIClient() client.serverURL = "" let url = client.streamURL(for: "/music/file.mp3") #expect(url == nil) } @Test("isConfigured false when no URL or key") @MainActor func notConfigured() { let client = ChadMusicAPIClient() client.serverURL = "" #expect(client.isConfigured == false) } } // MARK: - KeychainService Tests @Suite("KeychainService") struct KeychainServiceTests { @Test("Save and load API key roundtrip") func saveAndLoad() throws { // Clean up first KeychainService.deleteAPIKey() try KeychainService.saveAPIKey("test-key-12345") let loaded = KeychainService.loadAPIKey() #expect(loaded == "test-key-12345") // Clean up KeychainService.deleteAPIKey() } @Test("Load returns nil when no key stored") func loadNil() { KeychainService.deleteAPIKey() let loaded = KeychainService.loadAPIKey() #expect(loaded == nil) } @Test("Delete removes the key") func deleteKey() throws { try KeychainService.saveAPIKey("to-delete") KeychainService.deleteAPIKey() let loaded = KeychainService.loadAPIKey() #expect(loaded == nil) } @Test("Saving overwrites existing key") func overwrite() throws { KeychainService.deleteAPIKey() try KeychainService.saveAPIKey("old-key") try KeychainService.saveAPIKey("new-key") let loaded = KeychainService.loadAPIKey() #expect(loaded == "new-key") KeychainService.deleteAPIKey() } } // MARK: - ChadMusicError Tests @Suite("ChadMusicError") struct ChadMusicErrorTests { @Test("Error descriptions are non-empty") func errorDescriptions() { let errors: [ChadMusicError] = [ .notConfigured, .unauthorized, .forbidden, .notFound("test"), .httpError(500), .invalidResponse, .decodingFailed(NSError(domain: "test", code: 0)), .networkError(NSError(domain: "test", code: 0)), ] for error in errors { #expect(error.errorDescription != nil) #expect(!error.errorDescription!.isEmpty) } } }