| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- 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)
- }
- }
- }
|