Files
MusicPlayer/TESTING.md
T
Sansan f7619baf34 Use /api/v1 with token authentication and typed responses
Closes #38, #39 and #40; adds the client side of #41.

- Every request goes to /api/v1. MusicServerClient gets a token with
  Settings.api_password the first time one is needed, reuses it, and on a
  401 gets a new one and sends the request once more. Switching server gets
  a new token too. Callers never see the token.
- Sync is started with POST, as MusicServer 5.0 requires.
- The client has every endpoint in the server's OpenAPI spec, including
  statistics, soundtracks and songs by UUID, version, downloads and tokens.
- tools/generate_api_types.gd generates a typed class per schema in
  server/openapi.json into server/generated/. Song info, statistics and
  versions are passed to callers as these classes. A list the server sends
  as null is an empty list.
- FakeHttpTransport hands out tokens and answers 401 without one, so every
  test goes through the token flow. New tests cover the token handling,
  every endpoint and each generated class.
2026-10-06 22:45:28 +02:00

3.5 KiB

Testing

Tests use GdUnit4 6.2.1, committed in addons/gdUnit4/. The whole suite runs headless in about 10 seconds.

Running the tests

./run_tests.sh                                   # everything
./run_tests.sh res://tests/unit                  # only unit tests
./run_tests.sh res://tests/unit/test_search.gd   # one suite

The script finds Godot through $GODOT, then godot on your PATH, then /Applications/Godot.app or /Applications/Godot_mono.app. Reports are written to reports/ (ignored by git).

In the editor, open the GdUnit4 panel or right-click the tests folder and choose Run Tests.

CI runs the suite in the test job of .gitea/workflows/gitea-build.yaml on every push and pull request to main and develop. The build job only starts once the tests pass.

Layout

Folder Contents
tests/unit/ Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client
tests/integration/ Real scenes (MainWindow.tscn, SyncPopup.tscn) run against a fake server, including the point keybinds
tests/fakes/ FakeHttpTransport, AutoloadSnapshot and the MainWindowTestSuite base class
tests/data/ Files the fake server sends, e.g. a short MP3

Faking the MusicServer

All server calls go through the MusicServerClient autoload, which sends requests with a swappable transport. Tests install a FakeHttpTransport that answers from canned responses and records what was requested:

var server: FakeHttpTransport = FakeHttpTransport.new().install()
server.respond_json("/music/list", [{"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Frog's Theme"}])
server.respond_bytes("/music/rand", mp3_bytes)

# ... run the code under test ...

assert_int(server.count("/music/rand")).is_equal(1)
server.uninstall()

Paths are relative to the API (/music/list is <server>/api/v1/music/list). Paths without a response get a 404, so the code under test sees a failed request.

Like the real server, the fake hands out tokens on POST /token and answers 401 to requests without a valid one, so the client's token handling runs in every test. Token requests are counted in server.token_requests, not in requested_paths. Use server.expire_tokens() to make the client renew its token, server.hold("/token") to keep it waiting for one, and server.respond_json("/token", {...}, 401) for a refused password.

API classes

The classes in server/generated/ (SongInfo, StatisticsSummary, ...) are generated from the server's OpenAPI spec, committed as server/openapi.json. To update them, copy a new spec there (just openapi in MusicServer, or <server>/openapi.json) and run:

godot --headless --path . -s tools/generate_api_types.gd

tests/unit/test_api_types.gd parses a sample response with each class.

Writing tests

  • Settings and Playlist are global. Tests that change them restore them afterwards, either by hand or with AutoloadSnapshot.take([Settings, Playlist]) and restore().
  • Main window tests extend MainWindowTestSuite, which starts the window against the fake server and has helpers such as _add_player(), _remove_player() and _give_point_with_keybind().
  • Player ids are UUID strings that never change. A player's position in Settings.player_array only decides their keybind (Alt+1 for the first row and so on), so tests check both: that points land on the right player id, and that keybinds follow the rows when players are added or removed.