"Version X is available" in the top bar is now a button that opens an Update popup: - Update and restart gets the download link from the server (the redirect is not followed, as the Gitea link needs no token), downloads the new game next to the running one with a progress bar, checks it (complete, and an MZ or ELF file), swaps it in and restarts. The quiz is saved first and continues. On Windows the running .exe is renamed to .old, put back if the swap fails, and deleted at the next start. - Download in browser opens the .exe, the Linux package, or the release page on other platforms. - From the editor, an unwritable folder or another platform, only the browser download is offered, with the reason. The message now fits between the tabs and the buttons in every theme; in Nord and Christmas it ran into Sync. The macOS export preset is removed, and the unused download_client_* functions are replaced by fetch_client_download_url. Closes #13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
704 lines
26 KiB
GDScript
704 lines
26 KiB
GDScript
extends GdUnitTestSuite
|
|
## MusicServerClient: which paths it asks for, how it handles the answers and how it
|
|
## gets and renews its API token.
|
|
|
|
var _server: FakeHttpTransport
|
|
|
|
|
|
func before_test() -> void:
|
|
_server = FakeHttpTransport.new().install()
|
|
|
|
|
|
func after_test() -> void:
|
|
_server.uninstall()
|
|
|
|
|
|
func test_fetch_song_list_passes_song_infos() -> void:
|
|
var songs: Array = [{"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"}]
|
|
_server.respond_json("/music/list", songs)
|
|
var received: Array[SongInfo] = []
|
|
|
|
MusicServerClient.fetch_song_list(
|
|
func(data: Array[SongInfo]) -> void: received.append_array(data))
|
|
|
|
assert_int(received.size()).is_equal(1)
|
|
assert_int(received[0].song_no).is_equal(0)
|
|
assert_str(received[0].soundtrack).is_equal("Chrono Trigger")
|
|
assert_str(received[0].song).is_equal("Corridors of Time")
|
|
|
|
|
|
func test_fetch_song_info_passes_song_info() -> void:
|
|
_server.respond_json("/music/info", {"SongNo": 7, "Soundtrack": "Terranigma",
|
|
"Song": "Underworld", "SoundtrackUuid": "st-1", "SongUuid": "so-1"})
|
|
var received: Array[SongInfo] = []
|
|
|
|
MusicServerClient.fetch_song_info(func(info: SongInfo) -> void: received.append(info))
|
|
|
|
assert_int(received[0].song_no).is_equal(7)
|
|
assert_str(received[0].song_uuid).is_equal("so-1")
|
|
|
|
|
|
func test_empty_song_list_sent_as_null_gives_empty_array() -> void:
|
|
_server.respond_bytes("/music/list", "null".to_utf8_buffer())
|
|
var received: Array = []
|
|
|
|
MusicServerClient.fetch_song_list(func(data: Array[SongInfo]) -> void: received.append(data))
|
|
|
|
assert_int(received.size()).is_equal(1)
|
|
assert_array(received[0]).is_empty()
|
|
|
|
|
|
func test_json_list_that_is_not_an_array_calls_on_failed() -> void:
|
|
_server.respond_json("/music/list", {"error": "not a list"})
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.fetch_song_list(
|
|
func(_data: Array[SongInfo]) -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
@warning_ignore("unused_parameter")
|
|
func test_fetch_random_song_uses_the_endpoint_of_its_mode(mode: Settings.RandomMode,
|
|
path: String, test_parameters: Array = [
|
|
[Settings.RandomMode.NORMAL, "/music/rand"],
|
|
[Settings.RandomMode.LOW_PLAYED, "/music/rand/low"],
|
|
[Settings.RandomMode.CLASSIC, "/music/rand/classic"],
|
|
]) -> void:
|
|
MusicServerClient.fetch_random_song(mode, func(_body: PackedByteArray) -> void: pass)
|
|
|
|
assert_array(_server.requested_paths).contains_exactly([path])
|
|
|
|
|
|
func test_fetch_song_passes_raw_bytes() -> void:
|
|
var mp3: PackedByteArray = PackedByteArray([0x49, 0x44, 0x33])
|
|
_server.respond_bytes("/music?song=4", mp3)
|
|
var received: Array[PackedByteArray] = []
|
|
|
|
MusicServerClient.fetch_song(4, func(body: PackedByteArray) -> void: received.append(body))
|
|
|
|
assert_array(received).contains_exactly([mp3])
|
|
|
|
|
|
func test_download_url_is_where_the_server_redirects() -> void:
|
|
var link: String = "https://gitea.example/releases/download/2.0.0/MusicPlayer_2.0.0.exe"
|
|
_server.respond_redirect("/download/windows", link)
|
|
var urls: Array[String] = []
|
|
|
|
MusicServerClient.fetch_client_download_url("windows",
|
|
func(url: String) -> void: urls.append(url))
|
|
|
|
assert_array(urls).contains_exactly([link])
|
|
# The token is only for the server, so the redirect is not followed with it
|
|
assert_array(_server.requested_follow_redirects).contains_exactly([false])
|
|
|
|
|
|
func test_missing_download_calls_on_failed() -> void:
|
|
var failed: Array[bool] = []
|
|
|
|
MusicServerClient.fetch_client_download_url("linux-package",
|
|
func(_url: String) -> void: pass, func() -> void: failed.append(true))
|
|
|
|
assert_array(failed).has_size(1)
|
|
|
|
|
|
@warning_ignore("unused_parameter")
|
|
func test_header_value(name: String, value: String, test_parameters: Array = [
|
|
["Location", "https://x/y"],
|
|
["location", "https://x/y"],
|
|
["X-Queue-Id", "abc"],
|
|
["Missing", ""],
|
|
]) -> void:
|
|
var headers: PackedStringArray = ["Location: https://x/y", "x-queue-id: abc "]
|
|
|
|
assert_str(MusicServerClient.header_value(headers, name)).is_equal(value)
|
|
|
|
|
|
func test_failed_request_does_not_call_back() -> void:
|
|
_server.respond_json("/music/list", [], 500)
|
|
var called: Array[bool] = [false]
|
|
|
|
MusicServerClient.fetch_song_list(func(_data: Variant) -> void: called[0] = true)
|
|
|
|
assert_bool(called[0]).is_false()
|
|
|
|
|
|
func test_invalid_json_does_not_call_back() -> void:
|
|
_server.respond_bytes("/music/info", "not json".to_utf8_buffer())
|
|
var called: Array[bool] = [false]
|
|
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: called[0] = true)
|
|
|
|
assert_bool(called[0]).is_false()
|
|
|
|
|
|
func test_failed_request_calls_on_failed() -> void:
|
|
_server.respond_bytes("/music/rand", PackedByteArray(), 503)
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.fetch_random_song(Settings.RandomMode.NORMAL,
|
|
func(_body: PackedByteArray) -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
func test_invalid_json_calls_on_failed() -> void:
|
|
_server.respond_bytes("/music/info", "not json".to_utf8_buffer())
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.fetch_song_info(
|
|
func(_data: SongInfo) -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
func test_successful_request_does_not_call_on_failed() -> void:
|
|
_server.respond_bytes("/music/addQue", PackedByteArray())
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.add_to_queue(
|
|
func() -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(outcome).contains_exactly(["done"])
|
|
|
|
|
|
func test_send_calls_back_without_arguments() -> void:
|
|
_server.respond_bytes("/music/reset", PackedByteArray())
|
|
var called: Array[bool] = [false]
|
|
|
|
MusicServerClient.reset_playlist(func() -> void: called[0] = true)
|
|
|
|
assert_bool(called[0]).is_true()
|
|
|
|
|
|
func test_mark_song_played_sends_put() -> void:
|
|
_server.respond_bytes("/music/played?song=3", PackedByteArray())
|
|
|
|
MusicServerClient.mark_song_played(3)
|
|
|
|
assert_array(_server.requested_paths).contains_exactly(["/music/played?song=3"])
|
|
assert_array(_server.requested_methods).contains_exactly([HTTPClient.METHOD_PUT])
|
|
|
|
|
|
func test_character_name_is_url_encoded() -> void:
|
|
MusicServerClient.fetch_character_image("Paper Mario.png",
|
|
func(_body: PackedByteArray) -> void: pass)
|
|
|
|
assert_array(_server.requested_paths).contains_exactly(["/character?name=Paper%20Mario.png"])
|
|
|
|
|
|
func test_start_sync_sends_post() -> void:
|
|
_server.respond_json("/sync", "Sync started")
|
|
var called: Array[bool] = [false]
|
|
|
|
MusicServerClient.start_sync(func() -> void: called[0] = true)
|
|
|
|
assert_array(_server.requested_paths).contains_exactly(["/sync"])
|
|
assert_array(_server.requested_methods).contains_exactly([HTTPClient.METHOD_POST])
|
|
assert_bool(called[0]).is_true()
|
|
|
|
|
|
func test_start_full_sync_sends_post() -> void:
|
|
MusicServerClient.start_full_sync(func() -> void: pass)
|
|
|
|
assert_array(_server.requested_paths).contains_exactly(["/sync/full"])
|
|
assert_array(_server.requested_methods).contains_exactly([HTTPClient.METHOD_POST])
|
|
|
|
|
|
func test_sync_already_running_calls_on_failed() -> void:
|
|
_server.respond_json("/sync", {"error": "Sync is already running"}, 423)
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.start_sync(
|
|
func() -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
func test_requests_go_to_selected_server_under_api_path() -> void:
|
|
var previous_path: String = Settings.default_path
|
|
Settings.default_path = "http://localhost:8080"
|
|
var requested_urls: Array[String] = []
|
|
MusicServerClient.transport = _RecordingTransport.new(requested_urls)
|
|
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
|
|
Settings.default_path = previous_path
|
|
assert_array(requested_urls).contains_exactly([
|
|
"http://localhost:8080/api/v1/token",
|
|
"http://localhost:8080/api/v1/music/info",
|
|
])
|
|
|
|
|
|
# Tokens
|
|
|
|
func test_request_sends_token_in_authorization_header() -> void:
|
|
_server.respond_json("/music/info", {})
|
|
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
|
|
assert_int(_server.token_requests).is_equal(1)
|
|
assert_array(_server.requested_headers[0]) \
|
|
.contains_exactly(["Authorization: Bearer fake-token-1"])
|
|
|
|
|
|
func test_token_request_sends_password_and_client_type() -> void:
|
|
var previous_password: String = Settings.api_password
|
|
Settings.api_password = "secret"
|
|
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
|
|
Settings.api_password = previous_password
|
|
assert_dict(_server.last_token_request) \
|
|
.is_equal({"password": "secret", "client_type": "godot"})
|
|
|
|
|
|
func test_token_is_reused() -> void:
|
|
_server.respond_json("/music/info", {})
|
|
_server.respond_json("/music/list", [])
|
|
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
|
|
assert_int(_server.token_requests).is_equal(1)
|
|
assert_array(_server.requested_paths) \
|
|
.contains_exactly(["/music/info", "/music/list", "/music/info"])
|
|
|
|
|
|
func test_requests_while_getting_a_token_share_it() -> void:
|
|
_server.respond_json("/music/info", {})
|
|
_server.respond_json("/music/list", [])
|
|
_server.hold("/token")
|
|
var done: Array[String] = []
|
|
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: done.append("info"))
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: done.append("list"))
|
|
assert_array(_server.requested_paths).is_empty()
|
|
_server.release("/token")
|
|
|
|
assert_int(_server.token_requests).is_equal(1)
|
|
assert_array(done).contains_exactly(["info", "list"])
|
|
|
|
|
|
func test_expired_token_is_renewed_and_request_sent_again() -> void:
|
|
_server.respond_json("/music/info", {"SongNo": 3})
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
_server.expire_tokens()
|
|
var received: Array[SongInfo] = []
|
|
|
|
MusicServerClient.fetch_song_info(func(info: SongInfo) -> void: received.append(info))
|
|
|
|
assert_int(_server.token_requests).is_equal(2)
|
|
assert_array(_server.requested_paths) \
|
|
.contains_exactly(["/music/info", "/music/info", "/music/info"])
|
|
assert_array(_server.requested_headers[2]) \
|
|
.contains_exactly(["Authorization: Bearer fake-token-2"])
|
|
assert_int(received.size()).is_equal(1)
|
|
assert_int(received[0].song_no).is_equal(3)
|
|
|
|
|
|
func test_request_is_sent_again_only_once() -> void:
|
|
_server.respond_json("/music/info", {"error": "Unauthorized"}, 401)
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.fetch_song_info(
|
|
func(_data: SongInfo) -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_int(_server.token_requests).is_equal(2)
|
|
assert_int(_server.count("/music/info")).is_equal(2)
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
func test_refused_password_calls_on_failed_without_sending_request() -> void:
|
|
_server.respond_json("/token", {"error": "Invalid password"}, 401)
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.fetch_song_info(
|
|
func(_data: SongInfo) -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(_server.requested_paths).is_empty()
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
func test_rate_limited_token_request_calls_on_failed() -> void:
|
|
_server.respond_json("/token", {"error": "Too many requests"}, 429)
|
|
var outcome: Array[String] = []
|
|
|
|
MusicServerClient.add_to_queue(
|
|
func() -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(outcome).contains_exactly(["failed"])
|
|
|
|
|
|
func test_new_token_after_a_failed_token_request() -> void:
|
|
_server.respond_json("/token", {"error": "Too many requests"}, 429)
|
|
MusicServerClient.add_to_queue(func() -> void: pass)
|
|
_server.respond_json("/token", {"token": "second", "expires_at": "", "client_type": "godot"})
|
|
_server.respond_bytes("/music/addQue", PackedByteArray())
|
|
_server.expire_tokens()
|
|
|
|
MusicServerClient.add_to_queue(func() -> void: pass)
|
|
|
|
assert_int(_server.token_requests).is_equal(2)
|
|
assert_array(_server.requested_headers.back()) \
|
|
.contains_exactly(["Authorization: Bearer second"])
|
|
|
|
|
|
func test_changing_server_gets_a_new_token() -> void:
|
|
var previous_path: String = Settings.default_path
|
|
_server.respond_json("/music/info", {})
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
|
|
Settings.default_path = "http://another-server:8080"
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
Settings.default_path = previous_path
|
|
|
|
assert_int(_server.token_requests).is_equal(2)
|
|
|
|
|
|
func test_health_needs_no_token() -> void:
|
|
_server.respond_json("/health", {"status": "ok"})
|
|
var received: Array = []
|
|
|
|
MusicServerClient.fetch_health(func(data: Dictionary) -> void: received.append(data))
|
|
|
|
assert_int(_server.token_requests).is_equal(0)
|
|
assert_array(_server.requested_headers[0]).is_empty()
|
|
assert_array(received).contains_exactly([{"status": "ok"}])
|
|
|
|
|
|
func test_delete_token_sends_token_and_forgets_it() -> void:
|
|
_server.respond_json("/music/info", {})
|
|
MusicServerClient.fetch_song_info(func(_data: SongInfo) -> void: pass)
|
|
_server.respond_json("/token", {"status": "deleted"})
|
|
var called: Array[bool] = [false]
|
|
|
|
MusicServerClient.delete_token(func() -> void: called[0] = true)
|
|
|
|
assert_bool(called[0]).is_true()
|
|
assert_int(_server.requested_methods.back()).is_equal(HTTPClient.METHOD_DELETE)
|
|
assert_array(_server.requested_headers.back()) \
|
|
.contains_exactly(["Authorization: Bearer fake-token-1"])
|
|
|
|
|
|
func test_delete_token_without_token_sends_nothing() -> void:
|
|
var called: Array[bool] = [false]
|
|
|
|
MusicServerClient.delete_token(func() -> void: called[0] = true)
|
|
|
|
assert_bool(called[0]).is_true()
|
|
assert_array(_server.requested_paths).is_empty()
|
|
|
|
|
|
# Other endpoints
|
|
|
|
func test_endpoint_paths_and_methods() -> void:
|
|
var noop: Callable = func(_data: Variant = null) -> void: pass
|
|
var calls: Array[Array] = [
|
|
[func() -> void: MusicServerClient.fetch_next_song(noop),
|
|
"/music/next", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_previous_song(noop),
|
|
"/music/previous", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_sound_test_song(noop),
|
|
"/music/soundTest", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.mark_current_song_played(noop),
|
|
"/music/addPlayed", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_all_soundtracks(noop),
|
|
"/music/all", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_all_soundtracks_ordered(noop),
|
|
"/music/all/order", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_soundtracks(noop),
|
|
"/soundtracks", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_soundtrack("a b", noop),
|
|
"/soundtracks/a%20b", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_song_by_uuid("so-1", noop),
|
|
"/songs/so-1", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_song_file("so-1", noop),
|
|
"/songs/so-1/file", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_character_list(noop),
|
|
"/character/list", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_sync_progress(noop),
|
|
"/sync/progress", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_version(noop),
|
|
"/version", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_version_history(noop),
|
|
"/version/history", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_latest_client_version(noop),
|
|
"/download", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_client_assets(noop),
|
|
"/download/list", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_client_download_url("windows", noop),
|
|
"/download/windows", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_client_download_url("linux", noop),
|
|
"/download/linux", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.cleanup_tokens(noop),
|
|
"/token/cleanup", HTTPClient.METHOD_POST],
|
|
[func() -> void: MusicServerClient.fetch_soundtrack_statistics(noop),
|
|
"/statistics/soundtracks", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_statistics_summary(noop),
|
|
"/statistics/summary", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_most_played_soundtracks(5, noop),
|
|
"/statistics/soundtracks/most-played?limit=5", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_least_played_soundtracks(5, noop),
|
|
"/statistics/soundtracks/least-played?limit=5", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_last_played_soundtracks(5, noop),
|
|
"/statistics/soundtracks/last-played?limit=5", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_oldest_played_soundtracks(5, noop),
|
|
"/statistics/soundtracks/oldest-played?limit=5", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_never_played_soundtracks(noop),
|
|
"/statistics/soundtracks/never-played", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_most_played_songs(5, noop),
|
|
"/statistics/songs/most-played?limit=5", HTTPClient.METHOD_GET],
|
|
[func() -> void: MusicServerClient.fetch_least_played_songs(5, noop),
|
|
"/statistics/songs/least-played?limit=5", HTTPClient.METHOD_GET],
|
|
]
|
|
for entry: Array in calls:
|
|
_server.requested_paths.clear()
|
|
_server.requested_methods.clear()
|
|
entry[0].call()
|
|
assert_array(_server.requested_paths).contains_exactly([entry[1]])
|
|
assert_array(_server.requested_methods).contains_exactly([entry[2]])
|
|
|
|
|
|
func test_statistics_summary_is_typed() -> void:
|
|
_server.respond_json("/statistics/summary", {"total_soundtracks": 120,
|
|
"played_soundtracks": 100, "avg_soundtrack_plays": 2.5})
|
|
var received: Array[StatisticsSummary] = []
|
|
|
|
MusicServerClient.fetch_statistics_summary(
|
|
func(summary: StatisticsSummary) -> void: received.append(summary))
|
|
|
|
assert_int(received[0].total_soundtracks).is_equal(120)
|
|
assert_int(received[0].played_soundtracks).is_equal(100)
|
|
assert_float(received[0].avg_soundtrack_plays).is_equal(2.5)
|
|
|
|
|
|
func test_most_played_soundtracks_are_typed_with_their_songs() -> void:
|
|
_server.respond_json("/statistics/soundtracks/most-played?limit=2", [
|
|
{"soundtrack_uuid": "st-1", "soundtrack_name": "Chrono Trigger",
|
|
"soundtrack_played": 9, "songs": [{"song_name": "Frog's Theme"}]},
|
|
{"soundtrack_uuid": "st-2", "soundtrack_name": "Lufia II", "soundtrack_played": 4,
|
|
"songs": []},
|
|
])
|
|
var received: Array[SoundtrackWithSongs] = []
|
|
|
|
MusicServerClient.fetch_most_played_soundtracks(2,
|
|
func(list: Array[SoundtrackWithSongs]) -> void: received.append_array(list))
|
|
|
|
assert_int(received.size()).is_equal(2)
|
|
assert_str(received[0].soundtrack_name).is_equal("Chrono Trigger")
|
|
assert_int(received[0].soundtrack_played).is_equal(9)
|
|
assert_str(received[0].songs[0].song_name).is_equal("Frog's Theme")
|
|
assert_array(received[1].songs).is_empty()
|
|
|
|
|
|
func test_soundtrack_statistics_are_typed() -> void:
|
|
_server.respond_json("/statistics/soundtracks", [
|
|
{"soundtrack_name": "Chrono Trigger", "soundtrack_played": 2,
|
|
"songs": [{"song_name": "Frog's Theme", "times_played": 2}]},
|
|
{"soundtrack_name": "Lufia II", "soundtrack_played": 0, "songs": []},
|
|
])
|
|
var received: Array[SoundtrackWithSongs] = []
|
|
|
|
MusicServerClient.fetch_soundtrack_statistics(
|
|
func(list: Array[SoundtrackWithSongs]) -> void: received.append_array(list))
|
|
|
|
assert_int(received.size()).is_equal(2)
|
|
assert_str(received[0].songs[0].song_name).is_equal("Frog's Theme")
|
|
assert_int(received[1].soundtrack_played).is_equal(0)
|
|
|
|
|
|
func test_most_played_songs_are_typed() -> void:
|
|
_server.respond_json("/statistics/songs/most-played?limit=1", [
|
|
{"song_uuid": "so-1", "song_name": "Dancing Mad", "times_played": 12},
|
|
])
|
|
var received: Array[SongInfoForStats] = []
|
|
|
|
MusicServerClient.fetch_most_played_songs(1,
|
|
func(list: Array[SongInfoForStats]) -> void: received.append_array(list))
|
|
|
|
assert_str(received[0].song_name).is_equal("Dancing Mad")
|
|
assert_int(received[0].times_played).is_equal(12)
|
|
|
|
|
|
func test_statistics_calls_on_failed_on_server_error() -> void:
|
|
var outcome: Array[String] = []
|
|
var on_done: Callable = func(_data: Variant) -> void: outcome.append("done")
|
|
var on_failed: Callable = func() -> void: outcome.append("failed")
|
|
for path: String in ["/statistics/summary", "/statistics/soundtracks",
|
|
"/statistics/soundtracks/most-played?limit=3",
|
|
"/statistics/soundtracks/least-played?limit=3",
|
|
"/statistics/soundtracks/last-played?limit=3",
|
|
"/statistics/soundtracks/oldest-played?limit=3",
|
|
"/statistics/soundtracks/never-played",
|
|
"/statistics/songs/most-played?limit=3",
|
|
"/statistics/songs/least-played?limit=3"]:
|
|
_server.respond_json(path, {"error": "Internal Server Error"}, 500)
|
|
|
|
_call_every_statistics_endpoint(on_done, on_failed)
|
|
|
|
assert_array(outcome).contains_exactly(["failed", "failed", "failed", "failed", "failed",
|
|
"failed", "failed", "failed", "failed"])
|
|
|
|
|
|
func test_statistics_calls_on_failed_when_token_is_refused() -> void:
|
|
_server.respond_json("/token", {"error": "Invalid password"}, 401)
|
|
var outcome: Array[String] = []
|
|
|
|
_call_every_statistics_endpoint(func(_data: Variant) -> void: outcome.append("done"),
|
|
func() -> void: outcome.append("failed"))
|
|
|
|
assert_array(_server.requested_paths).is_empty()
|
|
assert_array(outcome).contains_exactly(["failed", "failed", "failed", "failed", "failed",
|
|
"failed", "failed", "failed", "failed"])
|
|
|
|
|
|
func _call_every_statistics_endpoint(on_done: Callable, on_failed: Callable) -> void:
|
|
MusicServerClient.fetch_statistics_summary(on_done, on_failed)
|
|
MusicServerClient.fetch_soundtrack_statistics(on_done, on_failed)
|
|
MusicServerClient.fetch_most_played_soundtracks(3, on_done, on_failed)
|
|
MusicServerClient.fetch_least_played_soundtracks(3, on_done, on_failed)
|
|
MusicServerClient.fetch_last_played_soundtracks(3, on_done, on_failed)
|
|
MusicServerClient.fetch_oldest_played_soundtracks(3, on_done, on_failed)
|
|
MusicServerClient.fetch_never_played_soundtracks(on_done, on_failed)
|
|
MusicServerClient.fetch_most_played_songs(3, on_done, on_failed)
|
|
MusicServerClient.fetch_least_played_songs(3, on_done, on_failed)
|
|
|
|
|
|
# Queue IDs
|
|
|
|
func test_no_queue_id_is_sent_before_the_server_sends_one() -> void:
|
|
_server.respond_json("/music/list", [])
|
|
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
|
|
assert_array(_server.requested_headers[0]) \
|
|
.contains_exactly(["Authorization: Bearer fake-token-1"])
|
|
|
|
|
|
func test_queue_id_from_a_response_is_sent_with_the_next_requests() -> void:
|
|
_server.respond_json("/music/list", [])
|
|
_server.respond_bytes("/music/addQue", PackedByteArray())
|
|
_server.queue_id = "queue-1"
|
|
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
MusicServerClient.add_to_queue(func() -> void: pass)
|
|
|
|
assert_str(MusicServerClient.queue_id()).is_equal("queue-1")
|
|
assert_array(_server.requested_headers[1]) \
|
|
.contains_exactly(["Authorization: Bearer fake-token-1", "X-Queue-Id: queue-1"])
|
|
DirAccess.remove_absolute(Settings.saved_settings_path())
|
|
|
|
|
|
func test_new_queue_id_replaces_the_old_one() -> void:
|
|
_server.respond_bytes("/music/reset", PackedByteArray())
|
|
_server.respond_json("/music/list", [])
|
|
_server.queue_id = "queue-1"
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
|
|
_server.queue_id = "queue-2"
|
|
MusicServerClient.reset_playlist(func() -> void: pass)
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
|
|
assert_array(_server.requested_headers[1]).contains(["X-Queue-Id: queue-1"])
|
|
assert_array(_server.requested_headers[2]).contains(["X-Queue-Id: queue-2"])
|
|
DirAccess.remove_absolute(Settings.saved_settings_path())
|
|
|
|
|
|
func test_queue_id_is_kept_per_server() -> void:
|
|
var previous_path: String = Settings.default_path
|
|
_server.respond_json("/music/list", [])
|
|
_server.queue_id = "queue-1"
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
|
|
_server.queue_id = ""
|
|
Settings.default_path = "http://another-server:8080"
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
var other_server_id: String = MusicServerClient.queue_id()
|
|
Settings.default_path = previous_path
|
|
|
|
assert_str(other_server_id).is_empty()
|
|
assert_array(_server.requested_headers[1]) \
|
|
.not_contains(["X-Queue-Id: queue-1"])
|
|
assert_str(MusicServerClient.queue_id()).is_equal("queue-1")
|
|
DirAccess.remove_absolute(Settings.saved_settings_path())
|
|
|
|
|
|
func test_queue_id_is_saved_for_the_next_start() -> void:
|
|
_server.respond_json("/music/list", [])
|
|
_server.queue_id = "queue-1"
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
|
|
Settings.queue_ids.clear()
|
|
assert_str(MusicServerClient.queue_id()).is_empty()
|
|
Settings.load_saved_settings()
|
|
|
|
assert_str(MusicServerClient.queue_id()).is_equal("queue-1")
|
|
DirAccess.remove_absolute(Settings.saved_settings_path())
|
|
|
|
|
|
func test_queue_id_header_name_is_case_insensitive() -> void:
|
|
var transport: _HeaderTransport = _HeaderTransport.new(["x-queue-id: lower-case"])
|
|
var previous_transport: HttpTransport = MusicServerClient.transport
|
|
MusicServerClient.transport = transport
|
|
|
|
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
|
|
MusicServerClient.transport = previous_transport
|
|
|
|
assert_str(MusicServerClient.queue_id()).is_equal("lower-case")
|
|
DirAccess.remove_absolute(Settings.saved_settings_path())
|
|
|
|
|
|
# Answers every request with 200, an empty JSON list and the given headers
|
|
class _HeaderTransport:
|
|
extends HttpTransport
|
|
|
|
var _headers: PackedStringArray
|
|
|
|
func _init(headers: PackedStringArray) -> void:
|
|
super(null)
|
|
_headers = headers
|
|
|
|
func request(
|
|
_url: String,
|
|
_method: int,
|
|
on_complete: Callable,
|
|
_request_headers: PackedStringArray = PackedStringArray(),
|
|
_body: PackedByteArray = PackedByteArray(),
|
|
_follow_redirects: bool = true,
|
|
) -> void:
|
|
on_complete.call(200, JSON.stringify({"token": "t"} if _url.ends_with("/token") else [])
|
|
.to_utf8_buffer(), _headers)
|
|
|
|
|
|
class _RecordingTransport:
|
|
extends HttpTransport
|
|
|
|
var _urls: Array[String]
|
|
|
|
func _init(urls: Array[String]) -> void:
|
|
super(null)
|
|
_urls = urls
|
|
|
|
func request(
|
|
url: String,
|
|
_method: int,
|
|
on_complete: Callable,
|
|
_headers: PackedStringArray = PackedStringArray(),
|
|
_body: PackedByteArray = PackedByteArray(),
|
|
_follow_redirects: bool = true,
|
|
) -> void:
|
|
_urls.append(url)
|
|
if url.ends_with("/token"):
|
|
on_complete.call(200, JSON.stringify({"token": "t"}).to_utf8_buffer(),
|
|
PackedStringArray())
|