import Domain import Foundation @testable import SourceSubsonic struct FixtureTransport: Sendable { var droppedAlbum: String? static func fixture(_ name: String) throws -> Data { let url = try unwrap(Bundle.module.url(forResource: name, withExtension: "json", subdirectory: "Fixtures")) return try Data(contentsOf: url) } static func empty(_ key: String) -> Data { Data(#"{"subsonic-response":{"status":"ok","version":"1.16.1","\#(key)":{}}}"#.utf8) } static func method(of request: URLRequest) -> (String, [String: String]) { let url = request.url ?? URL(filePath: "/") let method = url.lastPathComponent.replacingOccurrences(of: ".view", with: "") let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [] return (method, Dictionary(items.map { ($0.name, $0.value ?? "") }) { $1 }) } func callAsFunction(_ request: URLRequest) async throws -> (Data, URLResponse) { let (method, params) = Self.method(of: request) let response = HTTPURLResponse(url: request.url ?? URL(filePath: "/"), statusCode: 200, httpVersion: nil, headerFields: nil) return try (data(for: method, params: params), response ?? URLResponse()) } private func data(for method: String, params: [String: String]) throws -> Data { switch method { case "getArtists": try Self.fixture("artists") case "search3" where params["songOffset"] == "0": try Self.fixture("search3All") case "search3": Self.empty("searchResult3") case "getAlbumList2" where params["offset"] == "0": try albums() case "getAlbumList2": Self.empty("albumList2") case "getPlaylists": try Self.fixture("playlists") case "getPlaylist": try Self.fixture("playlist") case "getInternetRadioStations": try Self.fixture("radioStations") case "getStarred2": try Self.fixture("starred2") case "getLyricsBySongId": try Self.fixture("lyricsEmpty") default: try Self.fixture("ping") } } private func albums() throws -> Data { let data = try Self.fixture("albumList2All") guard let droppedAlbum else { return data } var root = try unwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) var response = try unwrap(root["subsonic-response"] as? [String: Any]) var list = try unwrap(response["albumList2"] as? [String: Any]) let albums = try unwrap(list["album"] as? [[String: Any]]) list["album"] = albums.filter { $0["id"] as? String != droppedAlbum } response["albumList2"] = list root["subsonic-response"] = response return try JSONSerialization.data(withJSONObject: root) } } extension SubsonicSource { static func fixture(transport: FixtureTransport = FixtureTransport(), id: SourceID = SourceID()) -> SubsonicSource { let client = SubsonicClient( baseURL: URL(string: "https://music.example.test")!, username: "h", password: "secret", transport: { try await transport($0) } ) return SubsonicSource(id: id, name: "Test", client: client) } func collect() async throws -> [Change] { var collected: [Change] = [] for try await change in changes(since: nil) { collected.append(change) } return collected } } private func unwrap(_ value: T?) throws -> T { guard let value else { throw FixtureError.missing } return value } private enum FixtureError: Error { case missing }