import XCTest @testable import MixBoard // MARK: - Chad Music Model Tests final class ChadTrackModelTests: XCTestCase { func testChadTrackDecoding() throws { let json = """ { "id": "a1b2c3d4e5f6", "title": "Echoes", "artist": "Pink Floyd", "album_artist": "Pink Floyd", "album": "Meddle", "duration": 1410.5, "no": 6, "url": "/music/Pink Floyd/Meddle/06 Echoes.flac", "bit_rate": 1024, "year": 1971, "cover": "/covers/42.jpg" } """.data(using: .utf8)! let track = try JSONDecoder().decode(ChadTrack.self, from: json) XCTAssertEqual(track.id, "a1b2c3d4e5f6") XCTAssertEqual(track.title, "Echoes") XCTAssertEqual(track.artist, "Pink Floyd") XCTAssertEqual(track.albumArtist, "Pink Floyd") XCTAssertEqual(track.album, "Meddle") XCTAssertEqual(track.duration, 1410.5) XCTAssertEqual(track.trackNumber, 6) XCTAssertEqual(track.no, 6) XCTAssertEqual(track.url, "/music/Pink Floyd/Meddle/06 Echoes.flac") XCTAssertEqual(track.bitRate, 1024) XCTAssertEqual(track.year, 1971) XCTAssertEqual(track.cover, "/covers/42.jpg") } func testChadTrackDecodingMinimalFields() throws { let json = """ { "id": "abc123", "title": "Unknown", "url": "/music/unknown.mp3" } """.data(using: .utf8)! let track = try JSONDecoder().decode(ChadTrack.self, from: json) XCTAssertEqual(track.id, "abc123") XCTAssertEqual(track.title, "Unknown") XCTAssertEqual(track.url, "/music/unknown.mp3") XCTAssertNil(track.artist) XCTAssertNil(track.album) XCTAssertNil(track.duration) XCTAssertNil(track.trackNumber) XCTAssertNil(track.bitRate) XCTAssertNil(track.year) XCTAssertNil(track.cover) } func testFormattedDuration() { let json = """ {"id": "x1", "title": "T", "url": "/t", "duration": 185.0} """.data(using: .utf8)! let track = try! JSONDecoder().decode(ChadTrack.self, from: json) XCTAssertEqual(track.formattedDuration, "3:05") } func testFormattedDurationNil() { let json = """ {"id": "x1", "title": "T", "url": "/t"} """.data(using: .utf8)! let track = try! JSONDecoder().decode(ChadTrack.self, from: json) XCTAssertEqual(track.formattedDuration, "—") } func testFormattedDurationZero() { let json = """ {"id": "x1", "title": "T", "url": "/t", "duration": 0} """.data(using: .utf8)! let track = try! JSONDecoder().decode(ChadTrack.self, from: json) XCTAssertEqual(track.formattedDuration, "0:00") } func testFormattedDurationLongTrack() { let json = """ {"id": "x1", "title": "T", "url": "/t", "duration": 3661} """.data(using: .utf8)! let track = try! JSONDecoder().decode(ChadTrack.self, from: json) XCTAssertEqual(track.formattedDuration, "61:01") } func testChadTrackHashable() throws { let json1 = """ {"id": "t1", "title": "A", "url": "/a"} """.data(using: .utf8)! let json2 = """ {"id": "t2", "title": "B", "url": "/b"} """.data(using: .utf8)! let track1 = try JSONDecoder().decode(ChadTrack.self, from: json1) let track2 = try JSONDecoder().decode(ChadTrack.self, from: json2) let set: Set = [track1, track2, track1] XCTAssertEqual(set.count, 2) } } final class ChadAlbumModelTests: XCTestCase { func testChadAlbumDecoding() throws { let json = """ { "id": "f8fbe6f9d852485b", "album": "Meddle", "artist": "Pink Floyd", "year": 1971, "genre": "Progressive Rock", "track_count": 6, "cover": "/covers/10.jpg", "publisher": "Harvest", "country": "UK", "total_duration": 2820.0 } """.data(using: .utf8)! let album = try JSONDecoder().decode(ChadAlbum.self, from: json) XCTAssertEqual(album.id, "f8fbe6f9d852485b") XCTAssertEqual(album.title, "Meddle") XCTAssertEqual(album.album, "Meddle") XCTAssertEqual(album.artist, "Pink Floyd") XCTAssertEqual(album.year, 1971) XCTAssertEqual(album.genre, "Progressive Rock") XCTAssertEqual(album.trackCount, 6) XCTAssertEqual(album.cover, "/covers/10.jpg") XCTAssertEqual(album.publisher, "Harvest") XCTAssertEqual(album.country, "UK") XCTAssertEqual(album.totalDuration, 2820.0) } func testChadAlbumMinimalDecoding() throws { let json = """ {"id": "abc", "album": "Untitled"} """.data(using: .utf8)! let album = try JSONDecoder().decode(ChadAlbum.self, from: json) XCTAssertEqual(album.id, "abc") XCTAssertEqual(album.title, "Untitled") XCTAssertNil(album.artist) XCTAssertNil(album.year) XCTAssertNil(album.trackCount) XCTAssertNil(album.cover) } } final class ChadCategoryModelTests: XCTestCase { func testChadCategoryDecoding() throws { let json = """ {"item": "Rock", "count": 42} """.data(using: .utf8)! let cat = try JSONDecoder().decode(ChadCategory.self, from: json) XCTAssertEqual(cat.id, "Rock") XCTAssertEqual(cat.name, "Rock") XCTAssertEqual(cat.item, "Rock") XCTAssertEqual(cat.count, 42) } func testChadCategoryDecodingNoCount() throws { let json = """ {"item": "Jazz"} """.data(using: .utf8)! let cat = try JSONDecoder().decode(ChadCategory.self, from: json) XCTAssertEqual(cat.name, "Jazz") XCTAssertNil(cat.count) } func testChadCategoryArrayDecoding() throws { let json = """ [ {"item": "Rock", "count": 100}, {"item": "Jazz", "count": 50}, {"item": "Electronic", "count": 75} ] """.data(using: .utf8)! let categories = try JSONDecoder().decode([ChadCategory].self, from: json) XCTAssertEqual(categories.count, 3) XCTAssertEqual(categories[0].name, "Rock") XCTAssertEqual(categories[2].count, 75) } } final class ChadStatsModelTests: XCTestCase { func testChadStatsDecoding() throws { let json = """ { "tracks": 1500, "albums": 120, "artists": 85, "duration": "3d 14h 22m" } """.data(using: .utf8)! let stats = try JSONDecoder().decode(ChadStats.self, from: json) XCTAssertEqual(stats.tracks, 1500) XCTAssertEqual(stats.albums, 120) XCTAssertEqual(stats.artists, 85) XCTAssertEqual(stats.duration, "3d 14h 22m") } func testChadStatsPartialDecoding() throws { let json = """ {"tracks": 500} """.data(using: .utf8)! let stats = try JSONDecoder().decode(ChadStats.self, from: json) XCTAssertEqual(stats.tracks, 500) XCTAssertNil(stats.albums) XCTAssertNil(stats.artists) XCTAssertNil(stats.duration) } } /// Album tracks endpoint returns a plain [ChadTrack] array — tested in ChadTrackModelTests. // MARK: - ChadCategoryType Tests final class ChadCategoryTypeTests: XCTestCase { func testAllCasesExist() { XCTAssertEqual(ChadCategoryType.allCases.count, 8) } func testRawValues() { XCTAssertEqual(ChadCategoryType.album.rawValue, "album") XCTAssertEqual(ChadCategoryType.artist.rawValue, "artist") XCTAssertEqual(ChadCategoryType.genre.rawValue, "genre") XCTAssertEqual(ChadCategoryType.year.rawValue, "year") XCTAssertEqual(ChadCategoryType.publisher.rawValue, "publisher") } func testDisplayNames() { XCTAssertEqual(ChadCategoryType.album.displayName, "Albums") XCTAssertEqual(ChadCategoryType.artist.displayName, "Artists") XCTAssertEqual(ChadCategoryType.genre.displayName, "Genres") } func testIcons() { XCTAssertFalse(ChadCategoryType.album.icon.isEmpty) XCTAssertFalse(ChadCategoryType.artist.icon.isEmpty) // Every category should have an icon for category in ChadCategoryType.allCases { XCTAssertFalse(category.icon.isEmpty, "\(category) should have an icon") } } func testIdentifiable() { let category = ChadCategoryType.album XCTAssertEqual(category.id, "album") } } // MARK: - Keychain Service Tests final class KeychainServiceTests: XCTestCase { override func tearDown() { super.tearDown() KeychainService.deleteAPIKey() } func testSaveAndLoadAPIKey() throws { try KeychainService.saveAPIKey("test-api-key-12345") let loaded = KeychainService.loadAPIKey() XCTAssertEqual(loaded, "test-api-key-12345") } func testLoadMissingAPIKey() { KeychainService.deleteAPIKey() let loaded = KeychainService.loadAPIKey() XCTAssertNil(loaded) } func testOverwriteAPIKey() throws { try KeychainService.saveAPIKey("old-key") XCTAssertEqual(KeychainService.loadAPIKey(), "old-key") try KeychainService.saveAPIKey("new-key") XCTAssertEqual(KeychainService.loadAPIKey(), "new-key") } func testDeleteAPIKey() throws { try KeychainService.saveAPIKey("to-delete") XCTAssertNotNil(KeychainService.loadAPIKey()) KeychainService.deleteAPIKey() XCTAssertNil(KeychainService.loadAPIKey()) } func testSaveEmptyKey() throws { try KeychainService.saveAPIKey("") // Empty string is still valid UTF-8 data let loaded = KeychainService.loadAPIKey() XCTAssertEqual(loaded, "") } func testSaveUnicodeKey() throws { let key = "api-key-with-ünîcödé-чëрт" try KeychainService.saveAPIKey(key) XCTAssertEqual(KeychainService.loadAPIKey(), key) } } // MARK: - ChadMusicAPIClient Tests (URL composition, no network) final class ChadMusicAPIClientTests: XCTestCase { override func tearDown() { super.tearDown() UserDefaults.standard.removeObject(forKey: "chadMusic.serverURL") UserDefaults.standard.removeObject(forKey: "chadMusic.apiKey") KeychainService.deleteAPIKey() } @MainActor func testStreamURLAbsolutePath() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com" let url = client.streamURL(for: "/music/Artist/Album/track.flac") XCTAssertNotNil(url) XCTAssertEqual(url?.scheme, "https") XCTAssertEqual(url?.host, "music.example.com") XCTAssertTrue(url?.path.contains("music/Artist/Album/track.flac") ?? false) } @MainActor func testStreamURLRelativePath() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com" let url = client.streamURL(for: "music/Artist/Album/track.flac") XCTAssertNotNil(url) XCTAssertTrue(url?.path.contains("music/Artist/Album/track.flac") ?? false) } @MainActor func testStreamURLWithTrailingSlash() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com/" let url = client.streamURL(for: "/music/track.flac") XCTAssertNotNil(url) XCTAssertTrue(url?.path.contains("music/track.flac") ?? false) } @MainActor func testStreamURLNotConfigured() { let client = ChadMusicAPIClient() client.serverURL = "" let url = client.streamURL(for: "/music/track.flac") XCTAssertNil(url) } @MainActor func testIsConfiguredFalseWhenEmpty() { let client = ChadMusicAPIClient() client.serverURL = "" KeychainService.deleteAPIKey() XCTAssertFalse(client.isConfigured) } @MainActor func testIsConfiguredFalseWithoutKey() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com" KeychainService.deleteAPIKey() XCTAssertFalse(client.isConfigured) } @MainActor func testIsConfiguredTrue() { let client = ChadMusicAPIClient() client.serverURL = "https://music.example.com" try! KeychainService.saveAPIKey("test-key") XCTAssertTrue(client.isConfigured) } @MainActor func testAuthHeaders() { let client = ChadMusicAPIClient() try! KeychainService.saveAPIKey("my-secret-key") let headers = client.authHeaders XCTAssertEqual(headers["Authorization"], "Bearer my-secret-key") } @MainActor func testAuthHeadersEmpty() { let client = ChadMusicAPIClient() KeychainService.deleteAPIKey() let headers = client.authHeaders XCTAssertTrue(headers.isEmpty) } } // MARK: - ChadMusicError Tests final class ChadMusicErrorTests: XCTestCase { func testErrorDescriptions() { XCTAssertNotNil(ChadMusicError.notConfigured.errorDescription) XCTAssertNotNil(ChadMusicError.unauthorized.errorDescription) XCTAssertNotNil(ChadMusicError.forbidden.errorDescription) XCTAssertNotNil(ChadMusicError.notFound("test").errorDescription) XCTAssertNotNil(ChadMusicError.httpError(500).errorDescription) XCTAssertNotNil(ChadMusicError.invalidResponse.errorDescription) // All errors should have non-empty descriptions XCTAssertFalse(ChadMusicError.notConfigured.errorDescription!.isEmpty) XCTAssertTrue(ChadMusicError.notFound("/api/test").errorDescription!.contains("/api/test")) XCTAssertTrue(ChadMusicError.httpError(503).errorDescription!.contains("503")) } func testUnauthorizedDescription() { let error = ChadMusicError.unauthorized XCTAssertTrue(error.errorDescription!.contains("401")) } }