# EL3AB test suite

```bash
./tests/run.sh
```

Requires `php` (8.1+), `node` (18+) and the Postgres client tools (`initdb`,
`pg_ctl`, `psql`). Everything runs against a **throwaway local Postgres** that
the script creates and destroys. **No production data is read or written.**

---

## What each test covers

### `test_swiss.php` — pairing engine and result logic
Simulates complete tournaments at 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32, 64 and 100
players and asserts the invariants after every round:

- nobody is ever paired with the same opponent twice
- every player is seated exactly once per round
- no player receives more than one bye
- colour allocation stays within 2 of even for everyone
- points are conserved (exactly 1.0 distributed per game)
- the tournament always reaches `completed`

It also checks that every result the server can derive is a value the
`public.match_result` enum actually accepts — the defect that silently rejected
every decisive game.

### `test_replay.php` — the move validator must not reject legal chess
Replays three real master games (Morphy's Opera Game, Kasparov–Topalov 1999, and
a game ending in promotion) through `chessValidateTransition`, using the same
`chess.min.js` the client ships. 167 transitions covering castling, promotion,
en passant, heavy capture sequences and two checkmates.

Asserts three things:
1. **no legal move is ever rejected** — a false positive here would break a live
   game, which is far worse than the tampering it prevents;
2. a move submitted by the player whose turn it is *not* is always refused;
3. replaying an earlier position is always refused.

It also checks that the winner of a checkmate is derived from the final position
alone, with no input from the client.

### `test_integration.php` — the engine against real Postgres
The parts only a real database can prove: `jsonb` round-trips as arrays rather
than escaped strings, the real enums accept what we write, starting a tournament
twice does not create a second round 1, and — the important one — **two players
racing to open the same board produce exactly one match**, because both derive
the same primary key and Postgres rejects the loser.

### `test_auth.php` — local JWT verification
The fast path that removes an upstream auth call from every request. Checks a
valid signature passes and that wrong-secret, expired, subject-less, malformed,
tampered-payload and `alg=none` tokens are all rejected.

### `test_e2e.mjs` — the whole API over HTTP
Drives two independent clients through the real endpoints, exactly as a browser
would. Fifteen sections, including:

- a queued player's hand-off survives them re-queueing (the bug that stranded
  one player alone in a game)
- the two players always receive **opposite** colours, and the server tells each
  client its own colour rather than letting it guess
- a non-participant cannot read a live game's position
- a move from the wrong player is refused; a retried move is reported as a
  duplicate rather than an error
- a full 33-move game to checkmate, move by move
- **a losing player who reports themselves the winner does not get the win**
- resignation, draw offers (which now require the opponent to have offered)
- both paired tournament players land on the same board with agreed colours
- a round advances by itself once its last game finishes
- the scheduler starts a due tournament, and forfeits no-shows so a round cannot
  stall forever
- the server clock flags an absent player, and does *not* flag one who is merely
  thinking
- heartbeats from the two players do not overwrite each other or clobber a
  standing draw offer

---

## How it works

`router.php` runs the **real** API files under PHP's built-in server, replacing
only the Supabase data helpers with `pgshim.php`, a `SupabaseClient` subclass
that speaks PostgREST filter syntax to a local Postgres. Authentication is *not*
stubbed: `includes/auth.php` runs unmodified and verifies tokens over HTTP
against `authstub.php`, so the auth path under test is the production one.

`schema.sql` mirrors the production types and tables. The enum values in it were
copied verbatim from the live PostgREST OpenAPI spec — if production ever gains
a new `match_result` or `time_control` value, update it there too.
