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

84 lines
3.5 KiB
Markdown

# Testing
Tests use [GdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) 6.2.1, committed in
`addons/gdUnit4/`. The whole suite runs headless in about 10 seconds.
## Running the tests
```bash
./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:
```gdscript
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:
```bash
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.