jevcrates.git / jev-mock / README.md
1# Chapter 21: jev-mock — a stunt double for Jev
2
3`jev-mock` is a pretend System One endpoint for tests. It is a real HTTP/2
4server with real TLS, listening on `127.0.0.1` behind a certificate
5authority it made up a moment ago, and it answers however the test tells it
6to. A client under test cannot tell it from the real thing, except that it
7is free, instant and never in a bad mood unless you ask it to be.
8
9It knows nothing about Postgres, jev-http or any other client: anything
10that speaks HTTP/2 over TLS can be tested against it.
11
12## Three ways to play the part
13
14```mermaid
15flowchart LR
16  C["client under test"] -->|"HTTP/2 + TLS"| M["MockJev"]
17  M -->|"Reply"| C
18  M --- R1["a closure:<br/>answer anything"]
19  M --- R2["replay::Replay:<br/>answer from a fixture"]
20  M --- R3["forward::Forward:<br/>ask the real API, record it"]
21  R3 -->|"real key added here"| J(("api.typesafe.ai"))
22  R3 -->|"fixture()"| F[("fixture JSON")]
23  F --> R2
24```
25
261. **From a closure.** `MockJev::start(|request| Reply::json(200, ...))`.
27   The closure sees each `Recorded` request (method, path, headers, body)
28   and returns a `Reply`: a status, headers, a JSON body, and optionally a
29   delay, a wait until N requests have arrived, or exact raw bytes.
302. **From a recording.** `replay::Replay` answers from a fixture, keyed by
31   the exact request bytes. A request the fixture lacks gets a 400 that
32   names it (`error_type` `fixture_miss`), and `assert_complete`, also run
33   when the last `Replay` handle drops, fails the test. A changed request
34   can never reach the network, and can never pass.
353. **By recording.** `forward::Forward` passes each request to a real
36   endpoint with the real API key and records the answer. The fixture it
37   builds (`fixture()`) then replays exactly what the endpoint said to
38   exactly those bytes. The
39   client under test talks to the mock with any key; the real one is added
40   by `Forward` and never recorded. It needs a multi-threaded tokio
41   runtime. `exchange(body)` sends and records a
42   request built elsewhere, without the mock. postjevsql's
43   `tools/record-gates --send` uses both.
44
45> **Aside.** The mock is also a witness. It counts TCP accepts, TLS
46> connections, requests abandoned before an answer (the client reset the
47> stream), the most requests in flight at once, and the client's HTTP/2
48> PINGs. It can send GOAWAY on every connection, advertise a
49> `SETTINGS_MAX_CONCURRENT_STREAMS` limit, or stop acknowledging PINGs on the
50> next N connections, so a test can check that a client's keep-alive really
51> notices a dead path.
52
53## Try it
54
55`jev-http`'s integration tests are the shortest real use:
56
57```
58cargo test -p jev-http --test mock
59```
60
61In a test of your own, on a tokio runtime:
62
63```rust
64let mock = jev_mock::MockJev::start(|_| jev_mock::Reply::json(200, answer)).await;
65// point the client at mock.endpoint() and trust mock.ca_file()
66assert_eq!(mock.requests().len(), 1);
67```
68
69## For the people who maintain it
70
71### In this folder
72
73| Path | What |
74| --- | --- |
75| [src/](src/) | The code: Chapter 21½. |
76| [Cargo.toml](Cargo.toml) | Dependencies: hyper and hyper-util (server), `tokio-rustls` with `ring`, `rcgen` for the throwaway CA, `tempfile` for the CA file, `serde_json`, tokio with a multi-threaded runtime. |
77| [CLAUDE.md](CLAUDE.md) | Invariants for agents. |
78
79← Previous: [Chapter 20¾: jev-http/tests](../jev-http/tests/) · Up: [jevcrates](../) · Next: [Chapter 21½: inside jev-mock/src](src/) →