import Foundation /// URL protocol that intercepts Chad Music API requests and returns canned responses. /// Activated only when `-MockNetwork` launch argument is present. final class MockURLProtocol: URLProtocol { /// Map of URL path suffixes to (statusCode, responseData) static var mockResponses: [String: (Int, Data)] = [:] static func registerMockResponses() { let stats = """ {"tracks":1234,"albums":56,"artists":78,"duration":"3d 12h"} """ let categories = """ [{"item":"Rock","count":42},{"item":"Jazz","count":15},{"item":"Electronic","count":30}] """ let years = """ [{"item":2024,"count":12},{"item":2023,"count":18},{"item":2012,"count":5}] """ let albums = """ [{"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}] """ let tracks = """ [{"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}] """ mockResponses["/api/stats"] = (200, Data(stats.utf8)) mockResponses["/api/cat/artist"] = (200, Data(categories.utf8)) mockResponses["/api/cat/genre"] = (200, Data(categories.utf8)) mockResponses["/api/cat/year"] = (200, Data(years.utf8)) mockResponses["/api/cat/publisher"] = (200, Data(categories.utf8)) mockResponses["/api/cat/country"] = (200, Data(categories.utf8)) mockResponses["/api/cat/type"] = (200, Data(categories.utf8)) mockResponses["/api/cat/status"] = (200, Data(categories.utf8)) mockResponses["/api/cat/album"] = (200, Data(albums.utf8)) mockResponses["/api/albums"] = (200, Data(albums.utf8)) mockResponses["/api/album/"] = (200, Data(tracks.utf8)) // prefix match for album tracks } /// Register a custom mock for a specific path (used by tests via launch environment) static func setMock(path: String, statusCode: Int, data: Data) { mockResponses[path] = (statusCode, data) } /// Register a malformed response to test error handling static func setMalformedMock(path: String) { mockResponses[path] = (200, Data("not valid json{{{".utf8)) } // MARK: - URLProtocol overrides override class func canInit(with request: URLRequest) -> Bool { guard let url = request.url else { return false } let path = url.path return mockResponses.keys.contains(where: { path.contains($0) }) } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let url = request.url else { client?.urlProtocol(self, didFailWithError: URLError(.badURL)) return } let path = url.path var matchedResponse: (Int, Data)? // Try exact match first, then prefix match if let exact = MockURLProtocol.mockResponses[path] { matchedResponse = exact } else { for (key, value) in MockURLProtocol.mockResponses where path.contains(key) { matchedResponse = value break } } guard let (statusCode, data) = matchedResponse else { client?.urlProtocol(self, didFailWithError: URLError(.fileDoesNotExist)) return } let response = HTTPURLResponse( url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"] )! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} }