| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- 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<ChadTrack> = [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")
- 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() throws {
- let client = ChadMusicAPIClient()
- client.serverURL = "https://music.example.com"
- try KeychainService.saveAPIKey("test-key")
- XCTAssertTrue(client.isConfigured)
- }
- @MainActor
- func testAuthHeaders() throws {
- 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"))
- }
- }
|