CloudStreamingTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import Foundation
  2. import Testing
  3. @testable import MixBoardiOS
  4. // MARK: - ChadMusic Model Tests
  5. @Suite("ChadMusic Models")
  6. struct ChadMusicModelTests {
  7. @Test("ChadCategory decodes correctly")
  8. func decodeChadCategory() throws {
  9. let json = """
  10. {"item": "Rock", "count": 42}
  11. """.data(using: .utf8)!
  12. let category = try JSONDecoder().decode(ChadCategory.self, from: json)
  13. #expect(category.item == "Rock")
  14. #expect(category.count == 42)
  15. #expect(category.id == "Rock")
  16. #expect(category.name == "Rock")
  17. }
  18. @Test("ChadCategory decodes with null count")
  19. func decodeChadCategoryNullCount() throws {
  20. let json = """
  21. {"item": "Jazz", "count": null}
  22. """.data(using: .utf8)!
  23. let category = try JSONDecoder().decode(ChadCategory.self, from: json)
  24. #expect(category.item == "Jazz")
  25. #expect(category.count == nil)
  26. }
  27. @Test("ChadAlbum decodes with snake_case keys")
  28. func decodeChadAlbum() throws {
  29. let json = """
  30. {
  31. "id": "abc123",
  32. "album": "Dark Side of the Moon",
  33. "artist": "Pink Floyd",
  34. "year": 1973,
  35. "genre": "Progressive Rock",
  36. "track_count": 10,
  37. "cover": "/covers/abc123.jpg",
  38. "publisher": "Harvest",
  39. "country": "UK",
  40. "type": "Album",
  41. "status": "Official",
  42. "total_duration": 2580.0,
  43. "original_date": "1973-03-01",
  44. "mb_id": "some-uuid"
  45. }
  46. """.data(using: .utf8)!
  47. let album = try JSONDecoder().decode(ChadAlbum.self, from: json)
  48. #expect(album.id == "abc123")
  49. #expect(album.title == "Dark Side of the Moon")
  50. #expect(album.artist == "Pink Floyd")
  51. #expect(album.year == 1973)
  52. #expect(album.genre == "Progressive Rock")
  53. #expect(album.trackCount == 10)
  54. #expect(album.totalDuration == 2580.0)
  55. }
  56. @Test("ChadAlbum title defaults to Untitled when album is null")
  57. func chadAlbumUntitled() throws {
  58. let json = """
  59. {"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}
  60. """.data(using: .utf8)!
  61. let album = try JSONDecoder().decode(ChadAlbum.self, from: json)
  62. #expect(album.title == "Untitled")
  63. }
  64. @Test("ChadTrack decodes with all fields")
  65. func decodeChadTrack() throws {
  66. let json = """
  67. {
  68. "id": "track1",
  69. "title": "Breathe",
  70. "artist": "Pink Floyd",
  71. "album_artist": "Pink Floyd",
  72. "album": "Dark Side of the Moon",
  73. "duration": 169.0,
  74. "no": 2,
  75. "url": "/music/Pink Floyd/DSOTM/02-Breathe.flac",
  76. "bit_rate": 1411,
  77. "year": 1973,
  78. "cover": "/covers/abc.jpg"
  79. }
  80. """.data(using: .utf8)!
  81. let track = try JSONDecoder().decode(ChadTrack.self, from: json)
  82. #expect(track.id == "track1")
  83. #expect(track.title == "Breathe")
  84. #expect(track.artist == "Pink Floyd")
  85. #expect(track.albumArtist == "Pink Floyd")
  86. #expect(track.album == "Dark Side of the Moon")
  87. #expect(track.duration == 169.0)
  88. #expect(track.trackNumber == 2)
  89. #expect(track.url == "/music/Pink Floyd/DSOTM/02-Breathe.flac")
  90. #expect(track.bitRate == 1411)
  91. #expect(track.year == 1973)
  92. }
  93. @Test("ChadTrack formatted duration")
  94. func trackFormattedDuration() throws {
  95. let json = """
  96. {"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}
  97. """.data(using: .utf8)!
  98. let track = try JSONDecoder().decode(ChadTrack.self, from: json)
  99. #expect(track.formattedDuration == "3:05")
  100. }
  101. @Test("ChadTrack formatted duration with nil shows dash")
  102. func trackFormattedDurationNil() throws {
  103. let json = """
  104. {"id": "t", "title": "T", "artist": null, "album_artist": null, "album": null, "duration": null, "no": null, "url": "/x", "bit_rate": null, "year": null, "cover": null}
  105. """.data(using: .utf8)!
  106. let track = try JSONDecoder().decode(ChadTrack.self, from: json)
  107. #expect(track.formattedDuration == "—")
  108. }
  109. @Test("ChadStats decodes")
  110. func decodeChadStats() throws {
  111. let json = """
  112. {"tracks": 5000, "albums": 400, "artists": 200, "duration": "3d 14h 22m"}
  113. """.data(using: .utf8)!
  114. let stats = try JSONDecoder().decode(ChadStats.self, from: json)
  115. #expect(stats.tracks == 5000)
  116. #expect(stats.albums == 400)
  117. #expect(stats.artists == 200)
  118. #expect(stats.duration == "3d 14h 22m")
  119. }
  120. @Test("ChadCategoryType has correct display names")
  121. func categoryDisplayNames() {
  122. #expect(ChadCategoryType.album.displayName == "Albums")
  123. #expect(ChadCategoryType.artist.displayName == "Artists")
  124. #expect(ChadCategoryType.genre.displayName == "Genres")
  125. #expect(ChadCategoryType.year.displayName == "Years")
  126. }
  127. @Test("ChadCategoryType has icons")
  128. func categoryIcons() {
  129. for category in ChadCategoryType.allCases {
  130. #expect(!category.icon.isEmpty)
  131. }
  132. }
  133. }
  134. // MARK: - Track.fromCloud Tests
  135. @Suite("Track Cloud Factory")
  136. struct TrackFromCloudTests {
  137. @Test("fromCloud creates track with correct fields")
  138. func fromCloudBasic() {
  139. let chadTrack = ChadTrack(
  140. id: "abc123",
  141. title: "Test Track",
  142. artist: "Test Artist",
  143. albumArtist: nil,
  144. album: "Test Album",
  145. duration: 240.0,
  146. no: 3,
  147. url: "/music/Test/Album/03-Track.flac",
  148. bitRate: 1411,
  149. year: 2024,
  150. cover: nil
  151. )
  152. let track = Track.fromCloud(chadTrack)
  153. #expect(track.title == "Test Track")
  154. #expect(track.artist == "Test Artist")
  155. #expect(track.album == "Test Album")
  156. #expect(track.duration == 240.0)
  157. #expect(track.isCloud == true)
  158. #expect(track.cloudStreamPath == "/music/Test/Album/03-Track.flac")
  159. #expect(track.cloudTrackId == "abc123")
  160. #expect(track.year == 2024)
  161. #expect(track.filePath == "")
  162. }
  163. @Test("fromCloud handles nil optional fields")
  164. func fromCloudNilFields() {
  165. let chadTrack = ChadTrack(
  166. id: "x",
  167. title: "Minimal",
  168. artist: nil,
  169. albumArtist: nil,
  170. album: nil,
  171. duration: nil,
  172. no: nil,
  173. url: "/music/file.mp3",
  174. bitRate: nil,
  175. year: nil,
  176. cover: nil
  177. )
  178. let track = Track.fromCloud(chadTrack)
  179. #expect(track.title == "Minimal")
  180. #expect(track.artist == "")
  181. #expect(track.album == "")
  182. #expect(track.duration == 0)
  183. #expect(track.isCloud == true)
  184. #expect(track.year == nil)
  185. }
  186. @Test("hasLocalFile is false for cloud tracks")
  187. func cloudTrackHasNoLocalFile() {
  188. let chadTrack = ChadTrack(
  189. id: "x",
  190. title: "Cloud",
  191. artist: nil,
  192. albumArtist: nil,
  193. album: nil,
  194. duration: 100,
  195. no: nil,
  196. url: "/music/file.mp3",
  197. bitRate: nil,
  198. year: nil,
  199. cover: nil
  200. )
  201. let track = Track.fromCloud(chadTrack)
  202. #expect(track.hasLocalFile == false)
  203. }
  204. }
  205. // MARK: - ChadMusicAPIClient Tests
  206. @Suite("ChadMusicAPIClient")
  207. struct ChadMusicAPIClientTests {
  208. @Test("streamURL builds correct URL")
  209. @MainActor
  210. func streamURLBuilding() {
  211. let client = ChadMusicAPIClient()
  212. client.serverURL = "https://music.example.com"
  213. let url = client.streamURL(for: "/music/Artist/Album/track.flac")
  214. #expect(url?.absoluteString == "https://music.example.com/music/Artist/Album/track.flac")
  215. }
  216. @Test("streamURL handles trailing slash on server")
  217. @MainActor
  218. func streamURLTrailingSlash() {
  219. let client = ChadMusicAPIClient()
  220. client.serverURL = "https://music.example.com/"
  221. let url = client.streamURL(for: "/music/file.mp3")
  222. #expect(url?.absoluteString == "https://music.example.com/music/file.mp3")
  223. }
  224. @Test("streamURL handles path without leading slash")
  225. @MainActor
  226. func streamURLNoLeadingSlash() {
  227. let client = ChadMusicAPIClient()
  228. client.serverURL = "https://music.example.com"
  229. let url = client.streamURL(for: "music/file.mp3")
  230. #expect(url?.absoluteString == "https://music.example.com/music/file.mp3")
  231. }
  232. @Test("streamURL returns nil for empty server")
  233. @MainActor
  234. func streamURLEmptyServer() {
  235. let client = ChadMusicAPIClient()
  236. client.serverURL = ""
  237. let url = client.streamURL(for: "/music/file.mp3")
  238. #expect(url == nil)
  239. }
  240. @Test("isConfigured false when no URL or key")
  241. @MainActor
  242. func notConfigured() {
  243. let client = ChadMusicAPIClient()
  244. client.serverURL = ""
  245. #expect(client.isConfigured == false)
  246. }
  247. }
  248. // MARK: - KeychainService Tests
  249. @Suite("KeychainService")
  250. struct KeychainServiceTests {
  251. @Test("Save and load API key roundtrip")
  252. func saveAndLoad() throws {
  253. // Clean up first
  254. KeychainService.deleteAPIKey()
  255. try KeychainService.saveAPIKey("test-key-12345")
  256. let loaded = KeychainService.loadAPIKey()
  257. #expect(loaded == "test-key-12345")
  258. // Clean up
  259. KeychainService.deleteAPIKey()
  260. }
  261. @Test("Load returns nil when no key stored")
  262. func loadNil() {
  263. KeychainService.deleteAPIKey()
  264. let loaded = KeychainService.loadAPIKey()
  265. #expect(loaded == nil)
  266. }
  267. @Test("Delete removes the key")
  268. func deleteKey() throws {
  269. try KeychainService.saveAPIKey("to-delete")
  270. KeychainService.deleteAPIKey()
  271. let loaded = KeychainService.loadAPIKey()
  272. #expect(loaded == nil)
  273. }
  274. @Test("Saving overwrites existing key")
  275. func overwrite() throws {
  276. KeychainService.deleteAPIKey()
  277. try KeychainService.saveAPIKey("old-key")
  278. try KeychainService.saveAPIKey("new-key")
  279. let loaded = KeychainService.loadAPIKey()
  280. #expect(loaded == "new-key")
  281. KeychainService.deleteAPIKey()
  282. }
  283. }
  284. // MARK: - ChadMusicError Tests
  285. @Suite("ChadMusicError")
  286. struct ChadMusicErrorTests {
  287. @Test("Error descriptions are non-empty")
  288. func errorDescriptions() {
  289. let errors: [ChadMusicError] = [
  290. .notConfigured,
  291. .unauthorized,
  292. .forbidden,
  293. .notFound("test"),
  294. .httpError(500),
  295. .invalidResponse,
  296. .decodingFailed(NSError(domain: "test", code: 0)),
  297. .networkError(NSError(domain: "test", code: 0)),
  298. ]
  299. for error in errors {
  300. #expect(error.errorDescription != nil)
  301. #expect(!error.errorDescription!.isEmpty)
  302. }
  303. }
  304. }