class_name FakeHttpTransport extends HttpTransport ## Stands in for the MusicServer in tests. ## ## Register canned responses per path with [method respond_json] or ## [method respond_bytes], then install it with [method install]. Requests to paths ## without a response get a 404. Responses are delivered immediately. ## ## Paths are relative to the API, e.g. [code]/music/list[/code] for ## [code]/api/v1/music/list[/code]. ## ## Like the real server it hands out tokens on [code]POST /token[/code] and answers 401 ## to requests without a valid one. Token requests are counted in ## [member token_requests], not in [member requested_paths]. ## ## Set [member queue_id] to answer with an [code]X-Queue-Id[/code] header, like the ## server's music endpoints. ## Paths that need no token, like on the real server. const OPEN_PATHS: Array[String] = ["/token", "/health"] ## Paths requested so far, relative to the API. var requested_paths: Array[String] = [] ## HTTP method of each request, in the same order as [member requested_paths]. var requested_methods: Array[int] = [] ## Headers of each request, in the same order as [member requested_paths]. var requested_headers: Array[PackedStringArray] = [] ## The body of each request as text, e.g. its JSON, empty for none, in the same order as ## [member requested_paths]. var requested_bodies: Array[String] = [] ## The body of each request as bytes, e.g. an uploaded picture, in the same order as ## [member requested_paths]. var requested_raw_bodies: Array[PackedByteArray] = [] ## Whether each request followed redirects, in the same order as ## [member requested_paths]. var requested_follow_redirects: Array[bool] = [] ## How many tokens have been asked for. var token_requests: int = 0 ## The JSON body of the last token request. var last_token_request: Dictionary = {} ## Sent in an X-Queue-Id header with every answer that is not about a token, unless empty. var queue_id: String = "" var _responses: Dictionary = {} var _successes_left: Dictionary = {} var _failures_left: Dictionary = {} var _held_paths: Dictionary = {} var _previous_transport: HttpTransport var _previous_queue_ids: Dictionary[String, String] var _valid_tokens: Array[String] = [] func _init() -> void: super(null) ## Replaces the MusicServerClient transport with this fake. func install() -> FakeHttpTransport: _previous_transport = MusicServerClient.transport MusicServerClient.transport = self MusicServerClient.clear_token() _previous_queue_ids = Settings.queue_ids.duplicate() Settings.queue_ids.clear() return self ## Restores the transport that was active before [method install]. func uninstall() -> void: MusicServerClient.transport = _previous_transport MusicServerClient.clear_token() Settings.queue_ids = _previous_queue_ids ## Answers requests to [param path] with a redirect to [param location], like the ## server's download routes. Requests that follow redirects get an empty 200 instead, as ## if they had followed it. func respond_redirect(path: String, location: String) -> void: _responses[path] = {"code": 302, "body": PackedByteArray(), "location": location} _successes_left.erase(path) ## Answers requests to [param path] with [param data] encoded as JSON. func respond_json(path: String, data: Variant, response_code: int = 200) -> void: respond_bytes(path, JSON.stringify(data).to_utf8_buffer(), response_code) ## Answers requests to [param path] with [param body] as-is. func respond_bytes(path: String, body: PackedByteArray, response_code: int = 200) -> void: _responses[path] = {"code": response_code, "body": body} _successes_left.erase(path) ## Answers the next [param failures] requests to [param path] with a 503, then as usual. func fail_first(path: String, failures: int) -> void: _failures_left[path] = failures ## Lets [param successes] more requests to [param path] succeed, then answers 503 until ## a new response is registered for the path. func fail_after(path: String, successes: int) -> void: _successes_left[path] = successes ## Holds back responses to [param path] until [method release] is called, like a slow ## server. func hold(path: String) -> void: _held_paths[path] = [] ## Delivers the responses held back for [param path] and stops holding it. func release(path: String) -> void: var waiting: Array = _held_paths.get(path, []) _held_paths.erase(path) for pending: Callable in waiting: pending.call() ## Returns how many times [param path] has been requested. func count(path: String) -> int: return requested_paths.count(path) ## Makes every token handed out so far invalid, like after 24 hours or a server restart. func expire_tokens() -> void: _valid_tokens.clear() func request( url: String, method: int, on_complete: Callable, headers: PackedStringArray = PackedStringArray(), body: PackedByteArray = PackedByteArray(), follow_redirects: bool = true, ) -> void: var path: String = url.trim_prefix(Settings.default_path + MusicServerClient.API_PATH) if path == "/token" && method == HTTPClient.METHOD_POST: token_requests += 1 if _held_paths.has(path): _held_paths[path].append( _answer_token_request.bind(body.get_string_from_utf8(), on_complete)) return _answer_token_request(body.get_string_from_utf8(), on_complete) return requested_paths.append(path) requested_methods.append(method) requested_headers.append(headers) requested_bodies.append(body.get_string_from_utf8()) requested_raw_bodies.append(body) requested_follow_redirects.append(follow_redirects) if !OPEN_PATHS.has(path.get_slice("?", 0)) && !_has_valid_token(headers): on_complete.call(401, JSON.stringify({"error": "Unauthorized"}).to_utf8_buffer(), PackedStringArray()) return if _held_paths.has(path): _held_paths[path].append(_answer.bind(path, on_complete, follow_redirects)) return _answer(path, on_complete, follow_redirects) func _answer(path: String, on_complete: Callable, follow_redirects: bool = true) -> void: var headers: PackedStringArray = [] if !queue_id.is_empty() && path != "/token": headers.append("X-Queue-Id: " + queue_id) if _failures_left.get(path, 0) > 0: _failures_left[path] -= 1 on_complete.call(503, PackedByteArray(), headers) return if _successes_left.has(path): if _successes_left[path] == 0: on_complete.call(503, PackedByteArray(), headers) return _successes_left[path] -= 1 if _responses.has(path) && _responses[path].has("location"): if follow_redirects: on_complete.call(200, PackedByteArray(), headers) else: headers.append("Location: " + _responses[path].location) on_complete.call(302, PackedByteArray(), headers) elif _responses.has(path): on_complete.call(_responses[path].code, _responses[path].body, headers) else: on_complete.call(404, PackedByteArray(), headers) func _answer_token_request(body: String, on_complete: Callable) -> void: var data: Variant = JSON.parse_string(body) last_token_request = data if data is Dictionary else {} if _responses.has("/token"): # A token from a registered response is valid too var registered: Dictionary = _responses["/token"] var response: Variant = JSON.parse_string(registered.body.get_string_from_utf8()) if registered.code == 200 && response is Dictionary: _valid_tokens.append(str(response.get("token", ""))) _answer("/token", on_complete) return var token: String = "fake-token-%d" % token_requests _valid_tokens.append(token) on_complete.call(200, JSON.stringify({ "token": token, "expires_at": "2026-10-07T12:00:00Z", "client_type": "godot", }).to_utf8_buffer(), PackedStringArray()) func _has_valid_token(headers: PackedStringArray) -> bool: for header: String in headers: if header.begins_with("Authorization: Bearer "): return _valid_tokens.has(header.trim_prefix("Authorization: Bearer ")) return false