MockURLProtocol.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import Foundation
  2. /// URL protocol that intercepts Chad Music API requests and returns canned responses.
  3. /// Activated only when `-MockNetwork` launch argument is present.
  4. final class MockURLProtocol: URLProtocol {
  5. /// Map of URL path suffixes to (statusCode, responseData)
  6. static var mockResponses: [String: (Int, Data)] = [:]
  7. static func registerMockResponses() {
  8. let stats = """
  9. {"tracks":1234,"albums":56,"artists":78,"duration":"3d 12h"}
  10. """
  11. let categories = """
  12. [{"item":"Rock","count":42},{"item":"Jazz","count":15},{"item":"Electronic","count":30}]
  13. """
  14. let years = """
  15. [{"item":2024,"count":12},{"item":2023,"count":18},{"item":2012,"count":5}]
  16. """
  17. let albums = """
  18. [{"id":"album-1","album":"Test Album","artist":"Test Artist","year":2024,"genre":"Rock","track_count":10,"cover":null,"publisher":null,"country":null,"type":"Album","status":"Official","total_duration":2400.0,"original_date":null,"mb_id":null},{"id":"album-2","album":"Second Album","artist":"Another Artist","year":2023,"genre":"Jazz","track_count":8,"cover":null,"publisher":null,"country":null,"type":"Album","status":"Official","total_duration":1800.0,"original_date":null,"mb_id":null}]
  19. """
  20. let tracks = """
  21. [{"id":"track-1","title":"First Track","artist":"Test Artist","album_artist":"Test Artist","album":"Test Album","duration":240.0,"no":1,"url":"/api/stream/track-1","bit_rate":320,"year":2024,"cover":null},{"id":"track-2","title":"Second Track","artist":"Test Artist","album_artist":"Test Artist","album":"Test Album","duration":180.0,"no":2,"url":"/api/stream/track-2","bit_rate":320,"year":2024,"cover":null}]
  22. """
  23. mockResponses["/api/stats"] = (200, Data(stats.utf8))
  24. mockResponses["/api/cat/artist"] = (200, Data(categories.utf8))
  25. mockResponses["/api/cat/genre"] = (200, Data(categories.utf8))
  26. mockResponses["/api/cat/year"] = (200, Data(years.utf8))
  27. mockResponses["/api/cat/publisher"] = (200, Data(categories.utf8))
  28. mockResponses["/api/cat/country"] = (200, Data(categories.utf8))
  29. mockResponses["/api/cat/type"] = (200, Data(categories.utf8))
  30. mockResponses["/api/cat/status"] = (200, Data(categories.utf8))
  31. mockResponses["/api/cat/album"] = (200, Data(albums.utf8))
  32. mockResponses["/api/albums"] = (200, Data(albums.utf8))
  33. mockResponses["/api/album/"] = (200, Data(tracks.utf8)) // prefix match for album tracks
  34. }
  35. /// Register a custom mock for a specific path (used by tests via launch environment)
  36. static func setMock(path: String, statusCode: Int, data: Data) {
  37. mockResponses[path] = (statusCode, data)
  38. }
  39. /// Register a malformed response to test error handling
  40. static func setMalformedMock(path: String) {
  41. mockResponses[path] = (200, Data("not valid json{{{".utf8))
  42. }
  43. // MARK: - URLProtocol overrides
  44. override class func canInit(with request: URLRequest) -> Bool {
  45. guard let url = request.url else { return false }
  46. let path = url.path
  47. return mockResponses.keys.contains(where: { path.contains($0) })
  48. }
  49. override class func canonicalRequest(for request: URLRequest) -> URLRequest {
  50. request
  51. }
  52. override func startLoading() {
  53. guard let url = request.url else {
  54. client?.urlProtocol(self, didFailWithError: URLError(.badURL))
  55. return
  56. }
  57. let path = url.path
  58. var matchedResponse: (Int, Data)?
  59. // Try exact match first, then prefix match
  60. if let exact = MockURLProtocol.mockResponses[path] {
  61. matchedResponse = exact
  62. } else {
  63. for (key, value) in MockURLProtocol.mockResponses where path.contains(key) {
  64. matchedResponse = value
  65. break
  66. }
  67. }
  68. guard let (statusCode, data) = matchedResponse else {
  69. client?.urlProtocol(self, didFailWithError: URLError(.fileDoesNotExist))
  70. return
  71. }
  72. let response = HTTPURLResponse(
  73. url: url,
  74. statusCode: statusCode,
  75. httpVersion: "HTTP/1.1",
  76. headerFields: ["Content-Type": "application/json"]
  77. )!
  78. client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
  79. client?.urlProtocol(self, didLoad: data)
  80. client?.urlProtocolDidFinishLoading(self)
  81. }
  82. override func stopLoading() {}
  83. }