jevcrates.git / jev-mock / src / README.md

Chapter 21½: inside jev-mock/src — the set, the script and the recorder

Three files: the server itself, and the two ways to give it someone else's lines.

The files, in reading order

  1. lib.rs: the server.
    • Recorded is one request as the server saw it: HTTP version, method, path, headers, body.
    • Reply is what to answer: Reply::json(status, body), then .header(name, value), .after(delay) (stall first; a client that gives up resets the stream, which abandoned() counts) and .after_requests(n) (stall until n requests have arrived, so a test can count what a client sends before its first answer without racing it). raw_body sends exact bytes instead of the JSON.
    • MockJev::start(respond) serves on 127.0.0.1 with a certificate for that address; start_for(names, respond) adds host names; start_limited(max_streams, respond) advertises SETTINGS_MAX_CONCURRENT_STREAMS. Each start makes a fresh CA with rcgen, writes it to a temp directory (ca_file()), and accepts only ALPN h2, as the real endpoint does.
    • Reading the result: endpoint(), endpoint_via(host), requests(), connections(), accepts(), peak_in_flight(), abandoned(), pings().
    • Making trouble: goaway() sends GOAWAY on every open connection; ignore_pings_on_next(n) makes the next n connections swallow their PING acknowledgements.
    • Frames, at the bottom, sits between TLS and hyper and reads the plaintext HTTP/2 stream frame by frame. hyper answers PINGs itself, so this is the only place the mock can count them or withhold the replies.
  2. replay.rs: Fixture and Replay. A fixture is JSON, {"model", "exchanges": [{"request", "responses": [{"status", "headers": [[name, value]], "body"}]}]}, with request and body as the bytes on the wire, as text. Fixture::parse, load and to_json read and write it. Replay::new(&fixture).responder() serves each request's responses in order; misses() lists what it lacked, and assert_complete() panics naming them, as the last handle's Drop does unless the test is already panicking.
  3. forward.rs: Forward::new(upstream, api_key, model) and its responder(), which sends each request upstream with block_in_place (so the mock must run on a multi-threaded tokio runtime), one fresh connection per request. Upstream it passes only content-type and x-typesafe-retry-count and sets its own authorization; from the answer it keeps only content-type, x-typesafe-request-id, retry-after and retry-after-ms. A retried request records its answers in order. A failure to reach the endpoint is answered with a 400 (forward_failed) and recorded as nothing. exchange(body) sends a request built elsewhere; fixture() returns everything recorded so far.

Try it

cargo test -p jev-http --test mock

jev-mock has no tests of its own; its users' tests exercise it.

For the people who maintain it

In this folder

FileWhat
lib.rsMockJev, Reply, Recorded, the frame watcher.
replay.rsFixture, Replay, Response.
forward.rsForward: record a fixture from a real endpoint.
CLAUDE.mdInvariants for agents.

← Previous: Chapter 21: jev-mock · Up: jev-mock · The end of the guide. Back to Chapter 16: jevcrates