# vlatte > Mock HTTP and gRPC APIs in Go tests, or serve mocks from files for apps in any language. vlatte is an API mocking framework at `github.com/caffeine-projects/vlatte`. There are two ways to use it: - In Go tests, `mockhttp` and `mockgrpc` start real HTTP and gRPC servers. A test adds mocks to a server and sends it requests through the code under test. The library requires Go 1.27 or later. - The `vlatte` CLI, a binary or a Docker image, serves HTTP and gRPC mocks from YAML or JSON mock files for applications in any language, records a real service and plays it back, and takes new mocks over an admin API. Running the release binary or image needs no Go installation. The installation commands and Docker examples use `v1.0.0`. Pin that version of the Go library, CLI or Docker image to match the examples. See [Installation](https://vlatte.dev/getting-started/installation.md). A mock is matchers, which say which requests it answers, and a reply, which says how it answers. The matchers, from the `match` package, are the same for both protocols. Everything a mock file says, Go can say too. A request no mock matches gets HTTP `418` with a `VLatte-Error` header, or gRPC `NotFound`, and a list of the mocks that came closest. --- Source: https://vlatte.dev/getting-started/index.md # Introduction VLatte mocks HTTP and gRPC APIs for tests and local development. Use it as a library inside Go tests, or run it as a standalone server for an app in any language, including web and mobile clients. Mocks match requests, return fixed or templated answers, simulate network failures, and record real services for later replay. You can check what your app sent through the request journal, and change a running server's mocks through its admin API or coding-agent tools. ## Two ways to use it | | In Go tests | With the `vlatte` CLI | |---|---|---| | Starts | `mockhttp.MustNew()` or `mockgrpc.MustNew()` in the test | `vlatte`, a binary or a [Docker image](https://vlatte.dev/run/docker.md) | | Mocks | Built in Go, or loaded from [mock files](https://vlatte.dev/http/mock-files.md) | [Mock files](https://vlatte.dev/http/mock-files.md), and the [admin API](https://vlatte.dev/admin-api/index.md) | | Fits | Unit and integration tests of Go code | Any language, local development, demos, CI environments | Both modes load the same mock files and share the matching, reply, recording and verification features. Go also supports callbacks and custom reply functions that a declarative file cannot hold. ## Choose your first workflow - **Go tests:** [Install the library](https://vlatte.dev/getting-started/installation.md#the-library), then write an [HTTP or gRPC test](https://vlatte.dev/getting-started/quick-start.md#http). - **Web, mobile or another language:** run the [standalone server](https://vlatte.dev/getting-started/quick-start.md#the-cli), or follow [Testing a frontend client](https://vlatte.dev/guides/testing-a-frontend-client.md). A binary or Docker image runs without a Go installation. ## Where to go next - [Install](https://vlatte.dev/getting-started/installation.md) the library and the CLI. - Write a first test in the [quick start](https://vlatte.dev/getting-started/quick-start.md). - Learn the few ideas everything else builds on in [Concepts](https://vlatte.dev/getting-started/concepts.md). - See where a server's [settings](https://vlatte.dev/getting-started/settings.md) come from: code, a `.vlatterc` file, `VLATTE_*` variables and flags. - Then read the section of your protocol, [HTTP](https://vlatte.dev/http/index.md) or [gRPC](https://vlatte.dev/grpc/index.md), and then the sections of the features both share, such as [Matchers](https://vlatte.dev/matchers/index.md), [Templating](https://vlatte.dev/templating/index.md) and [Scenarios](https://vlatte.dev/scenarios/index.md). --- Source: https://vlatte.dev/getting-started/installation.md # Installation Use the library inside Go tests, or install the standalone CLI for apps in any language. The release binary and Docker image need no Go installation on your machine. The commands below install `v1.0.0` of the library, CLI and Docker image. Pin a release version for repeatable builds. ## The library Requires Go 1.27 or later. ```sh go get github.com/caffeine-projects/vlatte@v1.0.0 ``` Import the packages you use: `mockhttp` for HTTP, `mockgrpc` for gRPC, and `match` for matchers. | Package | What it's for | |---|---| | `mockhttp` | HTTP mock servers | | `mockgrpc` | gRPC mock servers | | `match` | Request matchers, such as `match.Equal`, `match.JSONPath` and `match.All` | | `match/matchbuild` | The matcher lists of mock files, and the registry of custom matchers | | `match/matchscenario` | Scenario state for mock servers | | `webhook` | Calling a URL after a mock is served | | `fault` | Pipes that make an HTTP mock answer wrong | | `journal` | The request journal | | `admin` | The admin API | | `recipe//` | Recipes that prepare a server, such as `recipe/http/proxyall` | | `vlatte` | The types every mock server shares, and the Go template engine | ## The CLI ### Homebrew On macOS and Linux: ```sh brew install caffeine-projects/tap/vlatte ``` Homebrew installs the latest stable release. Run `vlatte version` to check the version installed. For a particular version, use its archive or Docker image, or `go install` with its published tag. ### An archive [GitHub Releases](https://github.com/caffeine-projects/vlatte/releases) has an archive for Linux, macOS and Windows, on `amd64` and `arm64`. Each holds the `vlatte` binary, which needs nothing else. This example installs the Linux `amd64` archive into your user's executable directory: ```sh VERSION=v1.0.0 curl -fsSL "https://github.com/caffeine-projects/vlatte/releases/download/$VERSION/vlatte_${VERSION#v}_linux_amd64.tar.gz" | tar -xz vlatte install -d "$HOME/.local/bin" install -m 755 vlatte "$HOME/.local/bin/vlatte" export PATH="$HOME/.local/bin:$PATH" vlatte version ``` Keep that directory on `PATH` in your shell configuration. For macOS, choose `darwin` and your CPU architecture instead; for Windows, extract the matching ZIP and put `vlatte.exe` in a directory on `PATH`. To run an extracted binary without installing it, use `./vlatte` on Linux or macOS, or `.\vlatte.exe` in PowerShell. ### Go Requires Go 1.27 or later. ```sh go install github.com/caffeine-projects/vlatte/cmd/vlatte@v1.0.0 vlatte version ``` The binary is written to `GOBIN`, or `$(go env GOPATH)/bin` when `GOBIN` is unset. Put that directory on `PATH` if your shell cannot find `vlatte`. ### Docker The image is `ghcr.io/caffeine-projects/vlatte`, for `linux/amd64` and `linux/arm64`: see [Docker](https://vlatte.dev/run/docker.md). To inspect the published release image: ```sh docker run --rm ghcr.io/caffeine-projects/vlatte:v1.0.0 version ``` ## Verifying a release The release workflow of this repository builds every archive and image, and signs them with [cosign](https://docs.sigstore.dev/cosign/) without a key: the signature names the workflow and the tag it ran for. Each archive also has an SBOM, `.sbom.json`, and a provenance attestation. An archive: check the signature of `checksums.txt`, then the archive against it. ```sh VERSION=v1.0.0 ARCHIVE="vlatte_${VERSION#v}_linux_amd64.tar.gz" gh release download "$VERSION" -R caffeine-projects/vlatte -p checksums.txt -p checksums.txt.sigstore.json -p "$ARCHIVE" cosign verify-blob checksums.txt \ --bundle checksums.txt.sigstore.json \ --certificate-identity "https://github.com/caffeine-projects/vlatte/.github/workflows/release.yml@refs/tags/$VERSION" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com sha256sum --ignore-missing -c checksums.txt # shasum -a 256 on macOS ``` Or, with the GitHub CLI alone, check its provenance: ```sh gh attestation verify "$ARCHIVE" -R caffeine-projects/vlatte ``` The image: ```sh cosign verify "ghcr.io/caffeine-projects/vlatte:$VERSION" \ --certificate-identity "https://github.com/caffeine-projects/vlatte/.github/workflows/release.yml@refs/tags/$VERSION" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com gh attestation verify "oci://ghcr.io/caffeine-projects/vlatte:$VERSION" -R caffeine-projects/vlatte ``` ## Versions Versions follow [semantic versioning](https://semver.org). In v1.x, patch releases fix bugs and minor releases add backward-compatible features. Breaking API changes require a new major version. A version with a suffix, such as `v0.1.0-rc.1`, is a release candidate. The changes of each version are on [GitHub Releases](https://github.com/caffeine-projects/vlatte/releases). Pin a published Go module version and the same Docker tag for repeatable builds; `@main` follows development. ## Editor support for mock files Mock files have JSON Schemas at stable addresses. Point your editor at them to get completion and validation: see [Mock file schemas](https://vlatte.dev/reference/schemas.md). --- Source: https://vlatte.dev/getting-started/quick-start.md # Quick start Choose [HTTP](#http) or [gRPC](#grpc) to embed VLatte in a Go test. For a client in another language, use [the CLI](#the-cli) to serve mock files; a binary or Docker image needs no Go installation. ## HTTP A test starts a server, adds a mock, and points the code under test at `m.URL(...)`: ```go func Example() { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() defer m.Close() scoped := m.MustMock(mockhttp.Getf("/hello").Reply(mockhttp.OK().PlainText("hello world"))) res, err := http.Get(m.URL("/hello")) if err != nil { panic(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.StatusCode, string(body), scoped.Called()) // Output: 200 hello world true } ``` In a test, use `t.Cleanup(m.Close)` in place of `defer`, and `scoped.AssertCalled(t)` to fail the test when the mock wasn't called: ```go func TestHello(t *testing.T) { m := mockhttp.MustNew() m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(mockhttp.Getf("/hello").Reply(mockhttp.OK().PlainText("hello world"))) // ... call m.URL("/hello") ... scoped.AssertCalled(t) } ``` `mockhttp.New` does the same as `MustNew` but returns an error instead of panicking. Pass `mockhttp.Setup()` to either one to configure the server: its address, TLS, CORS, proxying or recording, for example. ## gRPC A gRPC mock server serves any method of the services whose generated code the program imports. `InMemory()` needs no port: `m.Dial()` connects to it. ```go func Example() { m := mockgrpc.MustNew(mockgrpc.Setup().Silent().InMemory()) m.MustStart() defer m.Close() m.MustMock(mockgrpc.Method("TestService/Greetings"). Field("message", match.Equal("hi")). Reply(mockgrpc.Message(&pb.HiRes{Message: "hello"}))) res, err := pb.NewTestServiceClient(m.MustDial()).Greetings(context.Background(), &pb.HiReq{Message: "hi"}) if err != nil { panic(err) } fmt.Println(res.GetMessage()) // Output: hello } ``` ## The CLI Use a CLI built from the version of VLatte documented here; see [Installation](https://vlatte.dev/getting-started/installation.md). For a complete browser workflow without Go, follow [Testing a frontend client](https://vlatte.dev/guides/testing-a-frontend-client.md). Write a mock file: ```yaml # testdata/vlatte/_mocks/hello_mock.yaml request: method: GET path: /hello response: status: 200 body: hello world ``` Serve it: ```sh vlatte --addr=127.0.0.1:8080 ``` Leave the server running, then call it from another terminal: ```sh curl http://127.0.0.1:8080/hello # hello world ``` Point your app's development or test API base URL at `http://127.0.0.1:8080`. Browser apps may also need [CORS](https://vlatte.dev/http/cors.md); a device needs an [address it can reach](https://vlatte.dev/guides/testing-a-frontend-client.md#connect-a-mobile-app). ## Next - [Concepts](https://vlatte.dev/getting-started/concepts.md): mocks, matchers, replies and servers. - [HTTP](https://vlatte.dev/http/index.md) and [gRPC](https://vlatte.dev/grpc/index.md): everything each protocol does. - [Guides](https://vlatte.dev/guides/index.md): longer walkthroughs. --- Source: https://vlatte.dev/getting-started/concepts.md # Concepts Understand how servers, mocks and files fit together, where their working data lives, and what happens when no mock matches. ## Servers A mock server is a real server: `mockhttp.Server` for HTTP, `mockgrpc.Server` for gRPC. A Go test starts one, adds mocks, and sends it requests through the code under test. The standalone `vlatte` command starts the same servers for an app in any language. `Setup()` configures a server in Go, and its settings can also come from a `.vlatterc` file and `VLATTE_*` variables: see [Configuration](https://vlatte.dev/run/configuration.md). ## Mocks A mock is three things: 1. **Matchers** that say which requests it answers: a method, a path, a header, a field of the body. See [HTTP matching](https://vlatte.dev/http/matching.md), [gRPC matching](https://vlatte.dev/grpc/matching.md), and the [matchers](https://vlatte.dev/matchers/index.md) both share. 2. **A reply** that says how it answers: a status, headers, a body, a stream of messages, a proxy to a real service. See [HTTP replies](https://vlatte.dev/http/replies.md) and [gRPC replies](https://vlatte.dev/grpc/replies.md). 3. **Mock features** that both protocols share: an ID and a name, a priority, `Times` and `Once`, delays, [scenarios](https://vlatte.dev/scenarios/index.md), and [events such as webhooks](https://vlatte.dev/webhooks/index.md). See [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md). ```go m.MustMock(mockhttp.Postf("/orders"). // matchers Header("Content-Type", match.Equal("application/json")). Priority(1).Times(2). // mock features Reply(mockhttp.Created().JSON(map[string]any{"id": 1}))) // reply ``` When several mocks match a request, the one with the lowest priority number answers. ## Mock files Mocks can also live in YAML or JSON files, with the same parts as keys: `request`, `response`, `priority`, `scenario`, `on_complete`. A server loads them on start from its work directory, and the `vlatte` command serves them. See [HTTP mock files](https://vlatte.dev/http/mock-files.md) and [gRPC mock files](https://vlatte.dev/grpc/mock-files.md). ## The work directory A server keeps its own files under `testdata/vlatte`: mock files in `_mocks`, recordings in `_mocks_recorded`, and the CLI's admin mocks and journal. See [Work directory](https://vlatte.dev/run/configuration.md#work-directory). ## When nothing matches A server answers a request no mock matched itself, in a way that never looks like a mock's answer: HTTP `418` with a `VLatte-Error` header, gRPC `NotFound`. It lists the mocks that came closest. See [HTTP troubleshooting](https://vlatte.dev/http/troubleshooting.md) and [gRPC troubleshooting](https://vlatte.dev/grpc/troubleshooting.md). --- Source: https://vlatte.dev/getting-started/settings.md # Settings A server's settings, such as its address, its mock files or its log level, come from four places, each overriding the ones before it: 1. Code: `mockhttp.Setup()` or `mockgrpc.Setup()`, such as `Setup().Addr(":8080")`. 2. A `.vlatterc.yaml`, `.vlatterc.yml` or `.vlatterc.json` file in the working directory or in `testdata`. 3. `VLATTE_*` environment variables. 4. Command-line flags, for the `vlatte` command. Each setting has one key that the others derive from: `log.level` is nested under `log` in a file, as below, `VLATTE_LOG_LEVEL=debug` in the environment and `--log-level=debug` on the command line. ```yaml # .vlatterc.yaml addr: ":8080" log: level: debug ``` Servers in tests read the file and the environment too, unless `Setup().ConfigSources()` says otherwise. - [Configuration](https://vlatte.dev/run/configuration.md) covers the sources in full: how values are written, the root and work directories, and the sources in tests. - The [settings reference](https://vlatte.dev/reference/settings.md) lists every setting, with its flag, variable and file key. --- Source: https://vlatte.dev/run/index.md # Run vlatte runs outside Go tests too: the `vlatte` command serves mock files, for apps in any language. | Page | What it covers | |---|---| | [CLI](https://vlatte.dev/run/cli.md) | The `vlatte` command: serving mock files, recording, and the HTTP and gRPC servers it starts. | | [Configuration](https://vlatte.dev/run/configuration.md) | Settings from flags, `VLATTE_*` variables and `.vlatterc` files, and the work directory. | | [Docker](https://vlatte.dev/run/docker.md) | The image of the command, and running it with your mock files. | Every setting is listed in the [settings reference](https://vlatte.dev/reference/settings.md). A running server takes new mocks over the [admin API](https://vlatte.dev/admin-api/index.md), and from AI agents over [MCP](https://vlatte.dev/mcp/index.md). --- Source: https://vlatte.dev/run/cli.md # CLI `vlatte` serves the mock files under `testdata/vlatte/_mocks` in the working directory: ```sh vlatte --addr=127.0.0.1:8080 curl http://127.0.0.1:8080/hello ``` Without `--addr`, it listens on a random port and prints the URL. Settings can also come from a `.vlatterc` file or `VLATTE_*` environment variables; see [Configuration](https://vlatte.dev/run/configuration.md). Run `vlatte --help` to list them, and `vlatte completion --help` to set up shell completion. Mock files can send [webhooks](https://vlatte.dev/webhooks/index.md): the CLI registers the `webhook` listener, and `--webhook-timeout` bounds each request that sets no `timeout` of its own. Templates read the [key-value store](https://vlatte.dev/templating/index.md#key-value-store) as `.KV`: `--kv-file=kv.csv` fills it from a CSV file, for HTTP and gRPC mocks alike. It [records](https://vlatte.dev/recording/index.md) a service until it stops, and serves the recordings on the next run: ```sh vlatte --record --record-target=https://api.example.com --record-paths='/v1/*' # Ctrl-C writes the recordings into testdata/vlatte/_mocks_recorded; another Ctrl-C quits at once vlatte ``` It serves the [admin API](https://vlatte.dev/admin-api/index.md) too, on an address of its own, which it prints: a random local port unless `--admin-addr` sets one, and `--admin=false` turns it off. With `VLATTE_DOCKER_HOST` set, it listens on port 8081 of that host, as the mocks do on 8080. `POST /mocks` adds a mock sent in the request body, for example as JSON: ```sh vlatte --addr=127.0.0.1:8080 --admin-addr=127.0.0.1:8081 curl -X POST -H 'Content-Type: application/json' \ -d '{"request":{"method":"GET","path":"/added"},"response":{"status":201}}' \ http://127.0.0.1:8081/mocks ``` The command keeps the mocks its admin API adds in `{workdir}/_admin`, as [persistent admin mocks](https://vlatte.dev/admin-api/index.md#persistent-admin-mocks), so the next run serves them: `--admin-dir` moves them, and `--admin-dir=""` keeps them in memory. It serves gRPC mocks too, when a setting of the `grpc` section is set, as `--grpc-*` flags, `VLATTE_GRPC_*` variables, or `grpc:` in `.vlatterc`. It listens on `:50051`, unless `--grpc-addr` says, serves the services of the descriptor sets `--grpc-descriptors` names, from the mock files `--grpc-mock-files` names, with server reflection, an admin API of its own, which it prints, and a journal of its calls: ```sh protoc --descriptor_set_out=api.binpb --include_imports api.proto vlatte --grpc-descriptors=api.binpb --grpc-mock-files='mocks/grpc/*.yaml' grpcurl -plaintext -d '{"id": "u-1"}' localhost:50051 users.Users/Get ``` The gRPC admin API keeps the mocks it adds in `{workdir}/_grpc_admin`, as [persistent admin mocks](https://vlatte.dev/admin-api/index.md#persistent-admin-mocks), so the next run serves them: `--grpc-admin-dir` moves them, and `--grpc-admin-dir=""` keeps them in memory. `--grpc-tls` serves them over TLS, with a self-signed certificate, unless `--grpc-tls-cert` and `--grpc-tls-key` name the files of one; `--grpc-tls-ca` requires clients to present a certificate signed by a CA of its file. Each of these turns TLS on. It keeps a [journal](https://vlatte.dev/more/journal.md) of the requests it serves in `{workdir}/_journal`, across restarts, which the admin API queries: `--journal-dir` moves it, `--journal-dir=""` keeps it in a temporary directory, and `--journal=false` turns it off. Two commands can't share a journal directory: give each a `--name`, which gives it a [work directory](https://vlatte.dev/run/configuration.md#work-directory) of its own, `testdata/vlatte/`. The journal holds the requests it got, secrets included: its directory ignores its files in git. --- Source: https://vlatte.dev/run/configuration.md # Configuration A server's settings come from these sources, each overriding the ones before it: 1. Defaults. 2. Code: the options passed to `mockhttp.New`, such as `mockhttp.Setup().Addr(":8080")`. 3. A `.vlatterc.json`, `.vlatterc.yaml` or `.vlatterc.yml` file in the working directory or in `testdata`. `--config` or `VLATTE_CONFIG` names another file. 4. `VLATTE_*` environment variables. 5. Command-line flags, for the `vlatte` command. ## Keys, variables and flags Each setting has a key that its variable and flag derive from: `log.level` is nested under `log` in a file, as below, `VLATTE_LOG_LEVEL=debug` in the environment and `--log-level=debug` on the command line. `vlatte --help` lists every setting, as does the [settings reference](https://vlatte.dev/reference/settings.md). In a file, each setting has its type, as in mock files: `addr: ":8080"` in quotes, `max_age: 150` bare, and a list as a list. Environment variables and flags are text: - bools read as `strconv.ParseBool` reads them; - numbers are written in the JSON number grammar; - lists are comma-separated. `log.level` and `log.verbosity` take a name, in any case, such as `info`. ```yaml # .vlatterc.yaml addr: ":8080" log: level: debug proxy: timeout: 1.5s # or milliseconds: 1500 cors: true ``` Setting a key of a section (`cors`, `proxy`, `proxy_all`, `record`, `admin` or `journal`) turns the section on, and `false` turns it off. ## Root directory `root_dir` is the directory that relative mock file patterns and record directories start at. Absolute ones are used as they are. ## Work directory A server keeps its own files in its work directory: `testdata/vlatte`, or `testdata/vlatte/` for a server `name` names, so servers that share a directory keep apart. `workdir` (`--workdir`, `Setup().WorkDir(dir)`) sets another, as is, from `root_dir`. Without it, a server's name is one folder, which doesn't start with `_`, as vlatte's own do. ``` testdata/ vlatte/ _mocks/ mock files _mocks_recorded/ recordings _admin/ the mocks the CLI's admin API adds _grpc_admin/ the mocks the CLI's gRPC admin API adds _journal/ the CLI's journal test/ a server named "test": its own _mocks, _admin, _journal... ``` A path setting that starts with `{workdir}`, such as `mock_files`, `record.dir`, `admin.dir`, `grpc.admin.dir` or `journal.dir`, starts at the work directory: the defaults do, as `--help` shows. Other relative paths start at `root_dir`, or `grpc.root_dir` for a `grpc` setting. In a YAML file, quote such a value: `dir: "{workdir}/recordings"`. ## Errors Unknown keys, flags and `VLATTE_*` variables are errors, except `VLATTE_DOCKER_HOST`. A server that can't load its settings reports every problem at once: ``` server: loading the configuration sources: config: 2 problems env VLATTE_LOGLEVEL: unknown_key: VLATTE_LOGLEVEL is not a known setting; did you mean VLATTE_LOG_LEVEL? file .vlatterc.yaml: invalid_value: proxy.timeout must be milliseconds or a Go duration, not "10x"; such as 1500 or 1.5s ``` ## Sources in tests Servers in tests read the file and the environment too. Pick the sources with `ConfigSources`, or pass none to use code only: ```go m := mockhttp.MustNew(mockhttp.Setup().ConfigSources()) m = mockhttp.MustNew(mockhttp.Setup().ConfigSources(mockhttp.ConfigFile("testdata/server.yaml"))) ``` ## gRPC settings The settings of gRPC mocks are in the `grpc` section: `grpc.addr`, `grpc.descriptors`, `grpc.mock_files`, `grpc.record`, `grpc.admin` and the others, as `--grpc-*` flags and `VLATTE_GRPC_*` variables. In Go, `mockgrpc.Setup()` configures a gRPC server. See the [CLI](https://vlatte.dev/run/cli.md) and the [settings reference](https://vlatte.dev/reference/settings.md). --- Source: https://vlatte.dev/run/docker.md # Docker `ghcr.io/caffeine-projects/vlatte` is an image of the [CLI](https://vlatte.dev/run/cli.md) for `linux/amd64` and `linux/arm64`, run by a non-root user (`65532`). Each release tags it with its version, with and without the `v`, such as `v1.0.0` and `1.0.0`, and signs it: see [Verifying a release](https://vlatte.dev/getting-started/installation.md#verifying-a-release). These examples use the `v1.0.0` release image. ```sh docker run --rm -p 8080:8080 -p 8081:8081 -v "$PWD/testdata/vlatte/_mocks:/vlatte/testdata/vlatte/_mocks:ro" ghcr.io/caffeine-projects/vlatte:v1.0.0 ``` The image sets `VLATTE_DOCKER_HOST`, so it listens on every interface, on fixed ports: mocks on `8080`, or `8443` with `--https`, the admin API on `8081`, and gRPC mocks on `50051`, with their admin API on `8082`. Its working directory is `/vlatte`, so the CLI's defaults apply below it: mock files in `/vlatte/testdata/vlatte/_mocks`, and a `.vlatterc` file in `/vlatte` or `/vlatte/testdata`. Arguments go to `vlatte`, and `VLATTE_*` variables set the same [settings](https://vlatte.dev/run/configuration.md): ```sh docker run --rm -p 8080:8080 -e VLATTE_LOG_LEVEL=debug ghcr.io/caffeine-projects/vlatte:v1.0.0 --proxy-all-target=https://api.example.com docker run --rm ghcr.io/caffeine-projects/vlatte:v1.0.0 version ``` The CLI writes its journal and the mocks the admin API adds to `/vlatte/testdata/vlatte`, which the image's user owns. To keep them, or recordings, on the host, mount a `testdata` directory over `/vlatte/testdata` and run as its owner: `--user "$(id -u):$(id -g)" -v "$PWD/testdata:/vlatte/testdata"`. With a read-only root file system, give both of its writable paths a `tmpfs`: ```sh docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \ --tmpfs /tmp --tmpfs /vlatte/testdata/vlatte:uid=65532,gid=65532 \ -v "$PWD/testdata/vlatte/_mocks:/vlatte/testdata/vlatte/_mocks:ro" \ -p 8080:8080 -p 8081:8081 ghcr.io/caffeine-projects/vlatte:v1.0.0 ``` Its `HEALTHCHECK` runs `vlatte health`, which asks the admin API's readiness probe, `GET /readyz` on `8081`, and exits with 0 when it answers 200. With the admin API off or on another address, it checks that the mock server accepts connections on `8080` or `8443` instead, so it needs no settings of its own. Kubernetes can probe `GET /livez` and `GET /readyz` with `httpGet` too. ## Building the image The `Dockerfile` builds the image from a checkout, for the platforms of its distroless base. Docker builds the Go binary inside the image, so the host needs Docker and Git, but no Go installation: ```sh git clone https://github.com/caffeine-projects/vlatte.git cd vlatte docker build -t vlatte:dev . docker run --rm vlatte:dev version ``` Run the mock-file examples from your app's directory, using `vlatte:dev` to test your source build. --- Source: https://vlatte.dev/guides/index.md # Guides Each guide works through one task from start to end, with code you can run. The sections on [HTTP](https://vlatte.dev/http/index.md), [gRPC](https://vlatte.dev/grpc/index.md) and each feature say what each part does; the guides show the parts together. | Guide | What you build | |---|---| | [Testing a frontend client](https://vlatte.dev/guides/testing-a-frontend-client.md) | A mock API for a browser app without Go, with CORS, request verification and mobile connection notes. | | [Testing a REST client](https://vlatte.dev/guides/testing-a-rest-client.md) | Tests of a Go client of a users API: the happy path, an error answer, and what the client sent. | | [Mock files and the CLI](https://vlatte.dev/guides/mock-files-and-the-cli.md) | Mock files with editor support, served by the `vlatte` command and loaded by Go tests. | | [Record and replay](https://vlatte.dev/guides/record-and-replay.md) | Mock files recorded from a real API once and replayed in every later run. | | [Chaos testing](https://vlatte.dev/guides/chaos-testing.md) | Tests that a client retries a failing server, survives a reset connection, and gives up on a slow one. | | [Stateful flows](https://vlatte.dev/guides/stateful-flows.md) | A cart and checkout flow whose answers depend on what came before, tested in parallel. | | [Testing a gRPC streaming client](https://vlatte.dev/guides/grpc-streaming-client.md) | Tests of a client of a server stream, through errors and a server that dies midway. | | [Docker Compose](https://vlatte.dev/guides/docker-compose.md) | vlatte next to an app under test, with mocks changed over the admin API. | | [Testing webhooks](https://vlatte.dev/guides/webhooks.md) | A payment API that calls your service back, and a test that the callback came. | | [HTTPS and mTLS mocks](https://vlatte.dev/guides/https-mtls.md) | Tests of a client of an HTTPS-only API, with mutual TLS, and the CLI serving the same mock. | | [Testing an LLM streaming client](https://vlatte.dev/guides/llm-streaming.md) | Tests of a client of a chat API that streams its answers, through a dropped stream and a rate limit. | | [Mocking a remote MCP server](https://vlatte.dev/guides/mcp-server.md) | Mock files that play an MCP server over Streamable HTTP, and a test of an MCP client against them. | --- Source: https://vlatte.dev/guides/testing-a-frontend-client.md # Testing a frontend client Run VLatte as a local API for a web or mobile app, without installing Go. This guide serves a mock from a YAML file, calls it from a browser with `fetch`, and verifies the request through the admin API. It uses the [CLI](https://vlatte.dev/run/cli.md), [mock files](https://vlatte.dev/http/mock-files.md), [CORS](https://vlatte.dev/http/cors.md) and [request verification](https://vlatte.dev/verify/admin-api.md). ## Prepare the server Install Docker, then create a directory for the example. The commands below use the `v1.0.0` release image: ```sh mkdir -p frontend-example/mocks cd frontend-example ``` ## Create the mock Save this as `mocks/user_mock.yaml`: ```yaml # yaml-language-server: $schema=https://vlatte.dev/schemas/http.schema.json id: get-user request: method: GET path: /users/42 header: authorization: Bearer test-token response: status: 200 header: content-type: application/json body: id: 42 name: ann ``` Start the server in one terminal: ```sh docker run --rm -p 127.0.0.1:8080:8080 -p 127.0.0.1:8081:8081 \ -v "$PWD/mocks:/vlatte/testdata/vlatte/_mocks:ro" \ ghcr.io/caffeine-projects/vlatte:v1.0.0 --journal-dir="" \ --cors-allowed-origin=http://localhost:5173 ``` The mock API listens on port `8080`; the admin API listens on `8081`. `--journal-dir=""` keeps this run's requests in a temporary directory, so a previous run doesn't affect the count. The CORS origin is the address of the web app, including its scheme and port. ## Call it from a browser Save this as `index.html` next to the `mocks` directory: ```html Users client

Loading…

``` Serve the page with your development server on `http://localhost:5173`. For this standalone example, Python's server works too: ```sh python3 -m http.server 5173 --bind 127.0.0.1 ``` Open `http://localhost:5173` in the browser. The page should show `ann`. In an existing app, make the API base URL a development or test setting and point it at VLatte instead of the real API. The `Authorization` header makes the browser send an `OPTIONS` preflight before the `GET`. VLatte's CORS support answers that preflight and allows the requested header. It isn't a mock and isn't counted in the journal. A request made with `credentials: 'include'` also needs `--cors-allow-credentials` and an allowed origin; see [CORS](https://vlatte.dev/http/cors.md). ## Verify what the app sent From another terminal, check the method, path and token, and require one request: ```sh curl -X QUERY -H 'Content-Type: application/json' \ -d '{"method":"GET","path":"/users/42","header":{"authorization":"Bearer test-token"},"count":1,"within":"2s"}' \ http://127.0.0.1:8081/requests/verify ``` The answer's `passed` must be `true` and `count` must be `1`. A check that ran returns HTTP `200` even when it failed, so an automated test must assert the `passed` field too. `within` waits for the request to be journaled; it doesn't add a fixed pause to the test. Before running the page again, reset the server to clear the journal and reload the mock files: ```sh curl -X POST http://127.0.0.1:8081/reset ``` For tests that share a server, use [marks](https://vlatte.dev/verify/admin-api.md#marks) to select each test's own requests. To try an error screen, change the mock's response status to `503`, reload with `POST /mocks/reload`, and reload the page. The example displays `users: status 503`. ## Connect a mobile app The same mock files work with native clients. Make the app's API base URL configurable for the test build, then choose an address the device can reach: | Where the app runs | Address to use | |---|---| | Browser on the development machine | `http://127.0.0.1:8080` as above. | | Android Emulator | `http://10.0.2.2:8080`, the emulator's alias for the host's loopback interface. See [Android Emulator network addresses](https://developer.android.com/studio/run/emulator-networking-address). | | Physical device | The development machine's reachable network address, such as `http://192.168.1.20:8080`. The device's `localhost` refers to the device itself. | For a physical device, bind the mock API to the network interface and keep the admin API local: ```sh docker run --rm -p 8080:8080 -p 127.0.0.1:8081:8081 \ -v "$PWD/mocks:/vlatte/testdata/vlatte/_mocks:ro" \ ghcr.io/caffeine-projects/vlatte:v1.0.0 --journal-dir="" ``` Check that the device and host can reach each other and that the host firewall allows port `8080`. For other simulators or a WebView, check that runtime's host-routing and browser-origin rules. CORS applies to browsers and WebViews, rather than a native HTTP client. Network access and certificate trust are separate: an app can reach VLatte and still reject plain HTTP or an untrusted certificate. Android's [Network Security Configuration](https://developer.android.com/privacy-and-security/security-config) covers cleartext policy and debug CA trust. Apple's [App Transport Security](https://developer.apple.com/documentation/security/preventing-insecure-network-connections) covers secure connections and exceptions. Use a test certificate trusted by the app and valid for the hostname or IP it calls; [HTTPS and mTLS mocks](https://vlatte.dev/guides/https-mtls.md) shows how VLatte serves one. Keep test-only network settings out of release builds. ## What you used - [Mock files](https://vlatte.dev/http/mock-files.md) and their [schemas](https://vlatte.dev/reference/schemas.md). - The [CLI](https://vlatte.dev/run/cli.md), [Docker](https://vlatte.dev/run/docker.md), and [CORS](https://vlatte.dev/http/cors.md). - [Request verification through the admin API](https://vlatte.dev/verify/admin-api.md) and [reset](https://vlatte.dev/admin-api/index.md). --- Source: https://vlatte.dev/guides/testing-a-rest-client.md # Testing a REST client This guide tests a small Go client of a users API against a mock server: the happy path, an error answer, and what the client sent. It uses [mocks](https://vlatte.dev/http/mocks.md), [request matching](https://vlatte.dev/http/matching.md), [replies](https://vlatte.dev/http/replies.md) and [request verification](https://vlatte.dev/verify/index.md). ## The client ```go type User struct { ID int `json:"id"` Name string `json:"name"` } type Client struct { BaseURL string Token string } func (c *Client) Get(ctx context.Context, id int) (*User, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/users/%d", c.BaseURL, id), nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.Token) res, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("users: get %d: %s", id, res.Status) } var u User return &u, json.NewDecoder(res.Body).Decode(&u) } func (c *Client) Create(ctx context.Context, name string) error { body := strings.NewReader(fmt.Sprintf(`{"name": %q}`, name)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/users", body) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.Token) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { return fmt.Errorf("users: create: %s", res.Status) } return nil } ``` ## A server per test Each test starts its own server, so tests can run in parallel without sharing mocks. `Journal("")` keeps a journal of the requests in a temporary directory, which `Close` removes, and `Silent()` keeps the test output clean. ## The happy path The mock matches the path and the token, and answers JSON. `AssertCalled` fails the test when the client never called it, such as when it built a wrong URL. ```go func TestGetUser(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Journal("").Silent()) m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(mockhttp.Getf("/users/42"). Header("Authorization", match.Equal("Bearer secret")). Reply(mockhttp.OK().JSON(map[string]any{"id": 42, "name": "ann"}))) client := &Client{BaseURL: m.URL(), Token: "secret"} u, err := client.Get(t.Context(), 42) if err != nil { t.Fatal(err) } if u.Name != "ann" { t.Errorf("name = %q, want ann", u.Name) } scoped.AssertCalled(t) } ``` ## An error answer `Getf` takes a glob, so `/users/*` answers any user. The test checks that the client turns a `404` into an error. ```go func TestGetUserNotFound(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Journal("").Silent()) m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(mockhttp.Getf("/users/*").Reply(mockhttp.NotFound())) client := &Client{BaseURL: m.URL(), Token: "secret"} _, err := client.Get(t.Context(), 7) if err == nil || err.Error() != "users: get 7: 404 Not Found" { t.Fatalf("err = %v, want users: get 7: 404 Not Found", err) } scoped.AssertCalled(t) } ``` A request no mock matches gets `418` with a `VLatte-Error: no-match` header, never a status a real API sends, and a body that lists the mocks that came closest: see [Troubleshooting](https://vlatte.dev/http/troubleshooting.md). ## What the client sent The journal keeps every request, whatever answered it, and `VerifyRequests` checks them after the fact. `Times` checks how many requests a selection holds, here by their method, path and a field of their JSON body, and `Never` that no request went unmatched. A failure lists the requests that came closest, with the parts they missed. ```go func TestCreateUserSendsTheName(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Journal("").Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockhttp.Postf("/users").Reply(mockhttp.Created())) client := &Client{BaseURL: m.URL(), Token: "secret"} if err := client.Create(t.Context(), "bob"); err != nil { t.Fatal(err) } m.VerifyRequests(). Method("POST"). Path(match.Equal("/users")). Body(match.JSONPath("$.name", match.Equal("bob"))). Times(t, 1) m.VerifyRequests().Outcome(journal.OutcomeUnmatched).Never(t) } ``` ## What you used - `mockhttp.MustNew`, `Setup().Journal`, `Setup().Silent`: [HTTP](https://vlatte.dev/http/index.md), [Request journal](https://vlatte.dev/more/journal.md). - `VerifyRequests`, `Times`, `Never`: [Verifying requests](https://vlatte.dev/verify/index.md). - `Getf`, `Postf`, `Header`: [Mocks](https://vlatte.dev/http/mocks.md), [Request matching](https://vlatte.dev/http/matching.md). - `OK().JSON`, `NotFound`, `Created`: [Replies](https://vlatte.dev/http/replies.md). - `match.Equal`, `match.JSONPath`: [Matchers](https://vlatte.dev/matchers/index.md). --- Source: https://vlatte.dev/guides/mock-files-and-the-cli.md # Mock files and the CLI This guide writes mock files with editor support, serves them with the `vlatte` command, changes them while it runs, and loads the same files in Go tests. It uses [mock files](https://vlatte.dev/http/mock-files.md), [path templates](https://vlatte.dev/http/matching.md#path-templates), [templates](https://vlatte.dev/templating/index.md), the [CLI](https://vlatte.dev/run/cli.md) and the [admin API](https://vlatte.dev/admin-api/index.md). ## Write the files The command serves the files matching `testdata/vlatte/_mocks/*mock.yaml` and `*mock.yml`. The first line of each names its [schema](https://vlatte.dev/reference/schemas.md), so editors complete keys and flag mistakes as you type. ```yaml # yaml-language-server: $schema=https://vlatte.dev/schemas/http.schema.json # testdata/vlatte/_mocks/get-user_mock.yaml id: get-user name: get a user request: method: GET path_template: /users/{id} path_params: id: [regex, '^[0-9]+$'] response: status: 200 header: content-type: application/json body: '{"id": {{ .Request.PathParams.id }}, "name": "user {{ .Request.PathParams.id }}"}' template: enabled: true ``` ```yaml # yaml-language-server: $schema=https://vlatte.dev/schemas/http.schema.json # testdata/vlatte/_mocks/create-user_mock.yaml id: create-user request: method: POST path: /users header: content-type: application/json body: [jsonpath, $.name, [present]] response: status: 201 header_template: location: '/users/{{ randomInt 100 999 }}' body: '{"name": {{ (fromJSON .Request.Body.String).name | toJSON }}}' template: enabled: true ``` `path_template` matches `/users/7` and reads `7` as `.Request.PathParams.id`. The second file answers with the name the request sent, read with `fromJSON`, and `toJSON` writes it back as a JSON string. ## Serve them ```sh vlatte --addr=127.0.0.1:8080 --admin-addr=127.0.0.1:8081 ``` ```sh curl localhost:8080/users/7 # {"id": 7, "name": "user 7"} curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "ann"}' localhost:8080/users # HTTP/1.1 201 Created # Location: /users/124 # {"name": "ann"} ``` A request no mock matches gets `418`, and a body that says which mocks came closest and why they didn't match. It is Markdown, or JSON for a client that asks for it: see [near misses](https://vlatte.dev/http/troubleshooting.md#near-misses). ```md # Request did not match No mock matched `GET http://127.0.0.1:8080/nope`. Request ID: `0f8c2b1e-6d4a-4c3e-9a57-2b8e1d4f7c90` ## Closest mocks 1. `get-user` (`get a user`): 1 of 3 parts matched; differs in `path_template`, `path_params id` ## Next steps - Change the request, or the closest mock, in the parts they differ in. - See the request in the journal: `GET http://127.0.0.1:8081/requests/0f8c2b1e-6d4a-4c3e-9a57-2b8e1d4f7c90` ``` ## Change mocks while it runs The admin API takes mocks in the same format, as JSON or YAML: ```sh curl -X POST -H 'Content-Type: application/json' \ -d '{"request": {"method": "GET", "path": "/health-check"}, "response": {"status": 204}}' \ localhost:8081/mocks ``` The command keeps the mocks it adds across restarts, in `testdata/vlatte/_admin`. `POST /mocks/save` writes them as mock files into `_mocks`, to commit with the others. After editing a file, `POST /mocks/reload` loads the files again, or restart the command. The admin API also answers what the server got: `curl -X QUERY -H 'Content-Type: application/json' -d '{"method": "POST", "path": "/users", "limit": 0}' localhost:8081/requests` counts the requests to create users. ## Use the same files in Go tests A server made in Go loads the same folder on start, relative to the test's package directory: ```go func TestWithMockFiles(t *testing.T) { m := mockhttp.MustNew() m.MustStart() t.Cleanup(m.Close) res, err := http.Get(m.URL("/users/7")) if err != nil { t.Fatal(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } if res.StatusCode != http.StatusOK || string(body) != `{"id": 7, "name": "user 7"}` { t.Fatalf("status = %d, body = %s", res.StatusCode, body) } } ``` `Setup().MockFilePatterns(...)` loads other files, `m.MustMock(mockhttp.FromFile(path))` loads one, and `m.Reload()` loads them again. ## What you used - [Mock files](https://vlatte.dev/http/mock-files.md) and their [schemas](https://vlatte.dev/reference/schemas.md). - [Path templates](https://vlatte.dev/http/matching.md#path-templates) and [templates](https://vlatte.dev/templating/index.md). - The [CLI](https://vlatte.dev/run/cli.md) and the [admin API](https://vlatte.dev/admin-api/index.md). --- Source: https://vlatte.dev/guides/record-and-replay.md # Record and replay This guide records a real API once, commits what it answered as mock files, and replays them in every later test run, without the API. It uses [recording and playback](https://vlatte.dev/recording/index.md) and [mock files](https://vlatte.dev/http/mock-files.md). ## A server that records on demand The test below serves the recordings in `testdata/api`. With `-record`, it sends the request to `API_URL` instead and records the answer. [Install the library](https://vlatte.dev/getting-started/installation.md#the-library), then save this as `record_test.go` in a directory of its own inside your Go module: ```go package recording_test import ( "context" "flag" "io" "net/http" "os" "testing" "github.com/caffeine-projects/vlatte/mockhttp" ) var _record = flag.Bool("record", false, "record the API into testdata/api") func TestUsers(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup(). MockFilePatterns("testdata/api/*.mock.json"). RedactHeader("Authorization")) m.MustStart() t.Cleanup(m.Close) if *_record { target := os.Getenv("API_URL") if target == "" { t.Fatal("API_URL must name the upstream to record") } err := m.StartRecording( mockhttp.RecordDir("testdata/api"), mockhttp.RecordTarget(target), mockhttp.RecordTransform(func(r *mockhttp.RecordedMock) error { for i := range r.Responses { delete(r.Responses[i].Header, "X-Request-Id") } return nil }), ) if err != nil { t.Fatal(err) } // Cleanups run last first: the recordings are written before the server closes. t.Cleanup(func() { if _, err := m.StopRecording(context.Background()); err != nil { t.Error(err) } }) } req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, m.URL("/users/1"), nil) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "Bearer test-token") res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } if res.StatusCode != http.StatusOK || string(body) != `{"id":1}` { t.Fatalf("status = %d, body = %s", res.StatusCode, body) } } ``` The cleanup that stops recording uses `context.Background()` because the test's context is canceled before its cleanups run. It is registered after `m.Close`, so it writes the recordings before the server closes. ## Record, then replay without the upstream For a local walkthrough, start this small upstream in another terminal. It returns the response the test expects: ```sh python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer class API(BaseHTTPRequestHandler): def do_GET(self): if self.path != "/users/1": self.send_error(404) return self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(b'{"id":1}') HTTPServer(("127.0.0.1", 9000), API).serve_forever() PY ``` Record it once: ```sh API_URL=http://127.0.0.1:9000 go test . -run '^TestUsers$' -args -record ``` Check that `testdata/api` holds the recording, then stop the upstream with Ctrl-C and run the test again without `-record`: ```sh go test . -run '^TestUsers$' ``` The same status and body checks now pass using the files alone. For your own API, use its URL and client call, and assert the response your app depends on. Review the recordings before committing them. ## What a recording looks like Each call becomes a mock file, named after its request: ```json // testdata/api/GET-users-1-f20a0db5256583fc.mock.json { "id": "GET-users-1-f20a0db5256583fc", "name": "GET /users/1", "request": { "method": "GET", "url": "/users/1" }, "response": { "body": "{\"id\":1}", "header": { "Content-Type": [ "application/json" ] }, "status": 200 } } ``` A recording matches its request's method, path and query, and its body, and no other request. `RecordRequestHeaders` adds headers it matches, and calls it can't tell apart play their answers in turn. See [What a recording matches](https://vlatte.dev/recording/http.md#what-a-recording-matches). ## Keep secrets out Recordings are plain text, and they get committed: review them first. The server's `RedactHeader` keeps the `Authorization` header's value out, and `RecordTransform` drops a header that changes on every call. `Set-Cookie` is left out by default. See [Secrets](https://vlatte.dev/recording/files-secrets.md#secrets). ## Record with the CLI The `vlatte` command records the same way, from start to stop: ```sh vlatte --record --record-target=https://api.example.com --record-paths='/v1/*' # Ctrl-C writes the recordings into testdata/vlatte/_mocks_recorded vlatte # serves them ``` The admin API's `/recorder` page starts and stops a recording from a browser. See [Recording from the CLI](https://vlatte.dev/recording/cli.md), [Recording settings](https://vlatte.dev/recording/settings.md), and [Recording through the admin API](https://vlatte.dev/recording/admin-api.md). ## What you used - `StartRecording`, `StopRecording`, `RecordDir`, `RecordTarget`, `RecordTransform`: [Recording and playback](https://vlatte.dev/recording/index.md). - `Setup().MockFilePatterns`, `Setup().RedactHeader`: [Mock files](https://vlatte.dev/http/mock-files.md), [Troubleshooting](https://vlatte.dev/http/troubleshooting.md#logs). --- Source: https://vlatte.dev/guides/chaos-testing.md # Chaos testing This guide checks that a client retries a failing server, survives a reset connection, and gives up on a slow one. It uses [sequences](https://vlatte.dev/http/replies.md#sequences), [faults](https://vlatte.dev/http/faults.md) and [priorities](https://vlatte.dev/more/mock-lifecycle.md). ## The client `get` retries a `GET` up to three times, on a `5xx` answer or a network error: ```go // get retries a GET up to three times on a 5xx answer or a network error. func get(ctx context.Context, client *http.Client, url string) ([]byte, error) { var last error for range 3 { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } res, err := client.Do(req) if err != nil { last = err continue } body, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { last = err continue } if res.StatusCode >= 500 { last = fmt.Errorf("status %d", res.StatusCode) continue } return body, nil } return nil, last } ``` ## A server that recovers `Seq` answers each request with the next reply, then the one of `OnSequenceEnded`: two `503`s, then the report. `AssertNumberOfCalls` checks the client called three times. ```go func TestRetriesUntilTheServerRecovers(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(mockhttp.Getf("/report").Reply(mockhttp.Seq( mockhttp.ServiceUnavailable(), mockhttp.ServiceUnavailable(), ).OnSequenceEnded(mockhttp.OK().PlainText("done")))) body, err := get(t.Context(), http.DefaultClient, m.URL("/report")) if err != nil { t.Fatal(err) } if string(body) != "done" { t.Errorf("body = %q", body) } scoped.AssertNumberOfCalls(t, 3) } ``` ## A reset connection `fault.ConnectionResetByPeer()` resets the TCP connection before the status line. `Once()` serves it to the first request only; the next request falls through to the mock of priority 1, which answers. ```go func TestRetriesAResetConnection(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock( mockhttp.Getf("/report").Once().WirePipe(fault.ConnectionResetByPeer()).Reply(mockhttp.OK()), mockhttp.Getf("/report").Priority(1).Reply(mockhttp.OK().PlainText("done")), ) body, err := get(t.Context(), http.DefaultClient, m.URL("/report")) if err != nil { t.Fatal(err) } if string(body) != "done" { t.Errorf("body = %q", body) } } ``` Wire faults, such as resets, need HTTP/1.x: Go's client uses it against a plain `http://` server. ## A slow server `fault.Latency` delays every write of the answer. The client gives up after its timeout, and the test checks the error is a timeout: ```go func TestGivesUpOnASlowServer(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockhttp.Getf("/report"). WirePipe(fault.Latency(500*time.Millisecond, 0)). Reply(mockhttp.OK().PlainText("too late"))) client := &http.Client{Timeout: 100 * time.Millisecond} _, err := get(t.Context(), client, m.URL("/report")) if timeout, ok := errors.AsType[net.Error](err); !ok || !timeout.Timeout() { t.Fatalf("err = %v, want a timeout", err) } } ``` To slow only the body, use `Pipe` in place of `WirePipe`: the client then gets the status and headers at once. `fault.Bandwidth`, `fault.ChunkedDribbleDelay` and `fault.Timeout` shape a slow network in other ways: see [Faults](https://vlatte.dev/http/faults.md). ## The same in mock files ```yaml request: method: GET path: /report response: status: 200 body: done wire_pipes: - type: latency latency: 500ms ``` ## What you used - `Seq`, `OnSequenceEnded`: [Replies](https://vlatte.dev/http/replies.md#sequences). - `WirePipe`, `fault.ConnectionResetByPeer`, `fault.Latency`: [Pipes](https://vlatte.dev/http/pipes.md), [Faults](https://vlatte.dev/http/faults.md). - `Once`, `Priority`, `AssertNumberOfCalls`: [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md), [Mocks](https://vlatte.dev/http/mocks.md#asserting). --- Source: https://vlatte.dev/guides/stateful-flows.md # Stateful flows This guide mocks a cart and checkout flow, where an answer depends on what came before, and runs its tests in parallel against one server. It uses [scenarios](https://vlatte.dev/scenarios/index.md), scenario keys, [priorities](https://vlatte.dev/more/mock-lifecycle.md) and [templates](https://vlatte.dev/templating/index.md). ## The flow A scenario starts in `started`. Adding an item moves it to `has-items`; checking out requires `has-items` and moves it to `paid`. A checkout in any other state falls through to the mock of priority 1, which refuses it. The order's status reads the scenario's state in a template. `ScenarioKey` gives each value of `X-Test-Id` an instance of the scenario of its own, so tests that run at once each move their own. A key that renders empty plays in the shared instance. ## The tests The request helper sends each subtest's name as its `X-Test-Id`. The parent test creates one server and registers the flow before its subtests run in parallel: ```go func call(t *testing.T, m *mockhttp.Server, method, path string) (int, string) { t.Helper() req, err := http.NewRequestWithContext(t.Context(), method, m.URL(path), nil) if err != nil { t.Fatal(err) } req.Header.Set("X-Test-Id", t.Name()) res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } return res.StatusCode, strings.TrimSpace(string(body)) } func TestCheckout(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) const key = `{{ .Request.Header.Get "X-Test-Id" }}` m.MustMock( mockhttp.Postf("/cart/items"). StartScenario("checkout").ScenarioKey(key).ScenarioStateWillBe("has-items"). Reply(mockhttp.Created()), mockhttp.Postf("/checkout"). ScenarioIs("checkout").ScenarioKey(key).ScenarioStateIs("has-items").ScenarioStateWillBe("paid"). Reply(mockhttp.OK().JSON(map[string]any{"order": 1})), mockhttp.Postf("/checkout").Priority(1). Reply(mockhttp.Status(http.StatusConflict).PlainText("the cart is empty")), mockhttp.Getf("/orders/1"). Reply(mockhttp.OK().BodyTemplate(`{"status": "{{ .Scenarios.StateOf "checkout" (.Request.Header.Get "X-Test-Id") }}"}`)), ) t.Run("pays a cart with items", func(t *testing.T) { t.Parallel() call(t, m, http.MethodPost, "/cart/items") if status, _ := call(t, m, http.MethodPost, "/checkout"); status != http.StatusOK { t.Fatalf("checkout = %d", status) } if _, body := call(t, m, http.MethodGet, "/orders/1"); body != `{"status": "paid"}` { t.Errorf("order = %s", body) } }) t.Run("refuses an empty cart", func(t *testing.T) { t.Parallel() if status, _ := call(t, m, http.MethodPost, "/checkout"); status != http.StatusConflict { t.Fatalf("checkout = %d", status) } if _, body := call(t, m, http.MethodGet, "/orders/1"); body != `{"status": "started"}` { t.Errorf("order = %s", body) } }) } ``` ## Watching it move The admin API's `/scenario-board` page shows where each scenario and each of its keys is, and moves them with a click. In Go, `m.Scenarios(ctx)` lists them, `m.SetScenario(ctx, "checkout", "has-items", matchscenario.Key("t1"))` moves one, and `m.ResetScenarios()` starts every one over. ## The same in mock files ```yaml id: checkout scenario: name: checkout required_state: has-items new_state: paid key: '{{ .Request.Header.Get "X-Test-Id" }}' request: method: POST path: /checkout response: status: 200 body: order: 1 ``` ## What you used - `StartScenario`, `ScenarioIs`, `ScenarioStateIs`, `ScenarioStateWillBe`, `ScenarioKey`: [Scenarios](https://vlatte.dev/scenarios/index.md). - `Priority`: [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md). - `BodyTemplate`, `.Scenarios.StateOf`: [Templates](https://vlatte.dev/templating/index.md). --- Source: https://vlatte.dev/guides/grpc-streaming-client.md # Testing a gRPC streaming client This guide tests a client of a server stream: it reads every event, handles a stream that ends with an error status, and one whose server dies midway. Then it mocks a bidi chat. It uses [gRPC streaming](https://vlatte.dev/grpc/streaming.md), [cuts](https://vlatte.dev/grpc/cuts.md) and [matching](https://vlatte.dev/grpc/matching.md). ## The service ```proto syntax = "proto3"; package feed; message WatchRequest { string topic = 1; } message Event { string id = 1; } message Line { string text = 1; } service Feed { rpc Watch(WatchRequest) returns (stream Event); rpc Chat(stream Line) returns (stream Line); } ``` A gRPC mock server serves any method of the services whose generated code the test imports, here `pb`: it needs no implementation. ## The client `collect` reads the events of a topic until the stream ends: ```go func collect(ctx context.Context, client pb.FeedClient, topic string) ([]string, error) { stream, err := client.Watch(ctx, &pb.WatchRequest{Topic: topic}) if err != nil { return nil, err } var ids []string for { event, err := stream.Recv() if errors.Is(err, io.EOF) { return ids, nil } if err != nil { return ids, err } ids = append(ids, event.GetId()) } } ``` ## A server per test `InMemory()` serves on an in-memory listener: no port, and `m.Dial()` connects to it. The connection closes with the server. ## Reading every event `Messages` sends the events in turn, and `MessageDelay` spaces them out: ```go func TestCollectReadsEveryEvent(t *testing.T) { m := mockgrpc.MustNew(mockgrpc.Setup().InMemory().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockgrpc.Method("feed.Feed/Watch"). Field("topic", match.Equal("orders")). Reply(mockgrpc.Messages(&pb.Event{Id: "1"}, &pb.Event{Id: "2"}). MessageDelay(10 * time.Millisecond))) ids, err := collect(t.Context(), pb.NewFeedClient(m.MustDial()), "orders") if err != nil { t.Fatal(err) } if !slices.Equal(ids, []string{"1", "2"}) { t.Errorf("ids = %v", ids) } } ``` ## A stream that ends with an error `Status` ends the stream with a status after its messages. The client keeps what it read, and gets the status: ```go func TestCollectReturnsTheStatus(t *testing.T) { m := mockgrpc.MustNew(mockgrpc.Setup().InMemory().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockgrpc.Method("feed.Feed/Watch"). Reply(mockgrpc.Messages(&pb.Event{Id: "1"}).Status(codes.Unavailable, "restarting"))) ids, err := collect(t.Context(), pb.NewFeedClient(m.MustDial()), "orders") if status.Code(err) != codes.Unavailable || len(ids) != 1 { t.Fatalf("ids = %v, err = %v", ids, err) } } ``` ## A server that dies midway A `Func` reply sends what it wants through the call. Returning `vlatte.ErrReset` cuts the connection after the event it sent, before any status: the client reads `Unavailable`, and no trailer. ```go func TestCollectSurvivesACut(t *testing.T) { m := mockgrpc.MustNew(mockgrpc.Setup().InMemory().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockgrpc.Method("feed.Feed/Watch").Reply(mockgrpc.Func(func(_ context.Context, call *mockgrpc.Call) error { if err := call.Send(&pb.Event{Id: "1"}); err != nil { return err } return vlatte.ErrReset }))) ids, err := collect(t.Context(), pb.NewFeedClient(m.MustDial()), "orders") if status.Code(err) != codes.Unavailable || len(ids) != 1 { t.Fatalf("ids = %v, err = %v", ids, err) } } ``` ## A bidi chat A bidi call matches as it starts, on its method and metadata. Its `Bidi` reply then answers each message: `On` the messages a matcher matches, `Otherwise` the rest. ```go m.MustMock(mockgrpc.Method("feed.Feed/Chat"). Header("x-room", match.Equal("general")). Reply(mockgrpc.Bidi(). On(match.Field("text", match.Equal("ping")), mockgrpc.Message(&pb.Line{Text: "pong"})). Otherwise(mockgrpc.Status(codes.InvalidArgument, "unknown command")))) ``` ## What you used - `Setup().InMemory`, `MustDial`: [gRPC](https://vlatte.dev/grpc/index.md). - `Method`, `Field`, `Header`: [Methods](https://vlatte.dev/grpc/methods.md), [Request matching](https://vlatte.dev/grpc/matching.md). - `Messages`, `MessageDelay`, `Status`, `Bidi`: [Replies](https://vlatte.dev/grpc/replies.md), [Streaming](https://vlatte.dev/grpc/streaming.md). - `Func`, `vlatte.ErrReset`: [Cuts](https://vlatte.dev/grpc/cuts.md). --- Source: https://vlatte.dev/guides/docker-compose.md # Docker Compose This guide runs vlatte next to an app under test with Docker Compose: the app calls a mocked payments API, a test changes the mocks over the admin API, and the admin API asks for a token. It uses [Docker](https://vlatte.dev/run/docker.md), [mock files](https://vlatte.dev/http/mock-files.md), the [admin API](https://vlatte.dev/admin-api/index.md) and [outbound rules](https://vlatte.dev/http/proxying.md#outbound-rules). ## Prerequisites Install Docker with Docker Compose. This guide uses the `ghcr.io/caffeine-projects/vlatte:v1.0.0` release image. Put the files below in your app's directory, alongside the Dockerfile that builds your app. The app must read `PAYMENTS_URL` as its payments API base URL. For a self-contained browser example without an app image, use [Testing a frontend client](https://vlatte.dev/guides/testing-a-frontend-client.md). ## The files ``` . ├── compose.yaml ├── mocks/ │ └── payments_mock.yaml └── secrets/ └── admin_token ``` ```yaml # mocks/payments_mock.yaml # yaml-language-server: $schema=https://vlatte.dev/schemas/http.schema.json id: create-payment request: method: POST path: /payments response: status: 201 body: id: pay_1 status: authorized ``` Create the `mocks` and `secrets` directories, then generate the token with `openssl rand -hex 32 > secrets/admin_token`. ## The compose file ```yaml # compose.yaml services: payments: image: ghcr.io/caffeine-projects/vlatte:v1.0.0 ports: - "8080:8080" # mocks - "8081:8081" # admin API volumes: - ./mocks:/vlatte/testdata/vlatte/_mocks:ro environment: VLATTE_ADMIN_AUTH_TOKEN_FILE: /run/secrets/admin_token VLATTE_OUTBOUND_DENY: 169.254.0.0/16 secrets: - admin_token app: build: . environment: PAYMENTS_URL: http://payments:8080 depends_on: payments: condition: service_healthy secrets: admin_token: file: ./secrets/admin_token ``` - **The image** is pinned to `v1.0.0`. It listens on every interface, on fixed ports: mocks on `8080`, the admin API on `8081`, and gRPC mocks on `50051`, with their admin API on `8082`. - **Mock files** mount over `/vlatte/testdata/vlatte/_mocks`, which the command serves. - **The health check** of the image runs `vlatte health`, which asks the admin API's readiness probe. Probes need no credentials, so `service_healthy` works with the token set, and the app starts once the mocks are loaded. - **The token** comes from a file, as a Docker secret: the token has no flag, so it never shows on the command line. - **Outbound rules** keep the mocks from reaching cloud metadata at `169.254.169.254`, should a proxy reply or a webhook be pointed there. Add the ranges of your internal networks too, but not the range of the compose network if the mocks proxy to other services in it. Run `docker compose up --build --wait` from the app's directory to build the app and wait for the services to be ready. ## Changing mocks from a test The admin API takes a mock in the same format as a file. With the token: ```sh TOKEN=$(cat secrets/admin_token) curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"id": "declined", "priority": -1, "request": {"method": "POST", "path": "/payments"}, "response": {"status": 402, "body": {"status": "declined"}}}' \ localhost:8081/mocks # ... run the test of a declined payment ... curl -X POST -H "Authorization: Bearer $TOKEN" localhost:8081/reset ``` `POST /reset` removes the mocks the admin API added, loads the mock files again, starts every scenario over and clears the journal, so the next test starts clean. `QUERY /requests` checks what the app sent: see [Request journal](https://vlatte.dev/more/journal.md). The admin pages, such as `http://localhost:8081/`, ask for the token in the browser's sign-in prompt, as the password, with any username. ## Proxies in containers Docker can set `HTTP_PROXY` and `HTTPS_PROXY` in containers from `~/.docker/config.json`, and so send the hosts of the compose network, such as `http://app:8080`, to that proxy: add them to `NO_PROXY` in the `environment` of the service. ## What you used - The image, its ports and its health check: [Docker](https://vlatte.dev/run/docker.md). - `admin.auth.token_file`, `POST /mocks`, `POST /reset`: [Admin API](https://vlatte.dev/admin-api/index.md). - `outbound.deny`: [Outbound rules](https://vlatte.dev/http/proxying.md#outbound-rules). --- Source: https://vlatte.dev/guides/webhooks.md # Testing webhooks This guide mocks a payment API that calls your service back once a payment is made, and checks that the callback came. It uses [webhooks](https://vlatte.dev/webhooks/index.md), [templates](https://vlatte.dev/templating/index.md) and server data. ## Two servers One server stands for the payment API, the other for your service, which gets the callback. In a real test, your service is the code under test and only the payment API is a mock; here a second mock server stands in for it, so the test can check what the webhook sent. ```go func TestPayingAnOrderCallsTheShopBack(t *testing.T) { // shop stands for your service: it receives the payment API's callback. shop := mockhttp.MustNew(mockhttp.Setup().Silent()) shop.MustStart() t.Cleanup(shop.Close) callback := shop.MustMock(mockhttp.Postf("/payments/*/status"). Body(match.JSONPath("$.status", match.Equal("paid"))). Reply(mockhttp.NoContent())) // payments stands for the payment API. wh, err := webhook.New() require.NoError(t, err) payments := mockhttp.MustNew(mockhttp.Setup().Silent().OnCompleteListener("webhook", wh)) payments.MustStart() t.Cleanup(payments.Close) payments.SetData(map[string]any{"shop": shop.URL()}) payments.MustMock(mockhttp.Postf("/payments"). OnCompleteEvent(webhook.Options(). Method(http.MethodPost). URLTemplate(`{{ .Data.shop }}/payments/{{ (fromJSON .Request.Body.String).id }}/status`). Header("Content-Type", "application/json"). Body(`{"status": "paid"}`). Delay(100 * time.Millisecond). Build("webhook")). Reply(mockhttp.Accepted())) res, err := http.Post(payments.URL("/payments"), "application/json", strings.NewReader(`{"id": "p-1"}`)) require.NoError(t, err) io.Copy(io.Discard, res.Body) res.Body.Close() require.Equal(t, http.StatusAccepted, res.StatusCode) callback.AssertNumberOfCalls(t, 1) } ``` ## What makes it work - **The listener.** A mock names the listener of its `on_complete` event, here `webhook`, so the server registers a `webhook.WebHook` under that name first. The `vlatte` command registers one as `webhook` for you. - **Server data.** `SetData` gives templates `.Data`. The callback's address is only known once the shop server started, so it goes in data, and the webhook's `URLTemplate` reads it. - **The request.** The URL template reads the payment's id from the request body: `fromJSON` parses it, so the webhook calls `/payments/p-1/status`. - **No waiting.** The response ends once its webhooks are sent, delay included, so the test checks the callback right after it read the body. Read the body to its end first: a large or streamed body sends its headers before the webhooks are done. ## In a mock file The same mock, for the `vlatte` command. The command has no data to read, so the callback's address is written out, here as a shop on port 9000: ```yaml request: method: POST path: /payments response: status: 202 on_complete: - name: webhook parameters: method: POST url_template: 'http://localhost:9000/payments/{{ (fromJSON .Request.Body.String).id }}/status' header: content-type: application/json body: '{"status": "paid"}' delay: 100ms ``` ## When the callback fails A webhook that can't reach its target, or gets a status outside `2xx`, doesn't change the mock's reply: the payment still answers `202`. The server logs the failure as a warning, and the [journal](https://vlatte.dev/more/journal.md) keeps it in the request's `Message`, one line for each event, so a test checks a webhook failed without reading the logs. A test of the happy path asserts on the callback server, as above. --- Source: https://vlatte.dev/guides/https-mtls.md # HTTPS and mTLS mocks This guide tests a client of an API that serves HTTPS only, first with a certificate the client trusts, then with mutual TLS, where the client must present a certificate of its own. Then the `vlatte` command serves the same mock with the same certificates. It uses [TLS](https://vlatte.dev/tls/index.md) and [mock files](https://vlatte.dev/http/mock-files.md). ## The client ```go // Client calls the inventory API, which serves HTTPS only. type Client struct { BaseURL string HTTP *http.Client } func (c *Client) Stock(ctx context.Context, sku string) (int, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/stock/"+sku, nil) if err != nil { return 0, err } res, err := c.HTTP.Do(req) if err != nil { return 0, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return 0, fmt.Errorf("inventory: stock of %s: %s", sku, res.Status) } var body struct { Available int `json:"available"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { return 0, err } return body.Available, nil } ``` ## Certificates [mkcert](https://github.com/FiloSottile/mkcert) makes a CA and the certificates it signs. Put them in `testdata`, next to the tests: ```sh mkcert -cert-file testdata/localhost.pem -key-file testdata/localhost-key.pem localhost 127.0.0.1 mkcert -client -cert-file testdata/client.pem -key-file testdata/client-key.pem client cp "$(mkcert -CAROOT)/rootCA.pem" testdata/ca.pem ``` The server certificate names `127.0.0.1`, as `m.URL()` does. Keys of test CAs belong to tests only: never give one to a real service. Without certificates of your own, `m.MustStartTLS()` serves a test certificate the client must skip verifying. That tests less: a client that verifies certificates, as it should, needs one it can trust. ## The HTTP client `clientTLS` makes an HTTP client that trusts the test CA, with the client certificate or without it. Each test creates its mock server where its TLS settings are used. ```go // clientTLS trusts the test CA, and presents the client certificate when asked to. func clientTLS(t *testing.T, withCert bool) *http.Client { t.Helper() ca, err := os.ReadFile("testdata/ca.pem") require.NoError(t, err) pool := x509.NewCertPool() require.True(t, pool.AppendCertsFromPEM(ca)) conf := &tls.Config{RootCAs: pool} if withCert { cert, err := tls.LoadX509KeyPair("testdata/client.pem", "testdata/client-key.pem") require.NoError(t, err) conf.Certificates = []tls.Certificate{cert} } return &http.Client{Transport: &http.Transport{TLSClientConfig: conf}} } ``` ## Over HTTPS `TLSCertKeyPair` serves the server certificate, which the client verifies against the CA: ```go func TestStock_OverHTTPS(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent(). TLSCertKeyPair("testdata/localhost.pem", "testdata/localhost-key.pem")) m.MustStartTLS() t.Cleanup(m.Close) m.MustMock(mockhttp.Getf("/stock/sku-1").Reply(mockhttp.OK().JSON(map[string]any{"available": 7}))) client := &Client{BaseURL: m.URL(), HTTP: clientTLS(t, false)} n, err := client.Stock(t.Context(), "sku-1") require.NoError(t, err) require.Equal(t, 7, n) } ``` ## With mutual TLS `TLSMutual` serves the same certificate, and asks the client for one the CA signed. The client with a certificate gets the stock; the one without is refused before any request is sent, so no mock sees it: ```go func TestStock_WithMutualTLS(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent(). TLSMutual("testdata/localhost.pem", "testdata/localhost-key.pem", "testdata/ca.pem")) m.MustStartTLS() t.Cleanup(m.Close) m.MustMock(mockhttp.Getf("/stock/sku-1").Reply(mockhttp.OK().JSON(map[string]any{"available": 7}))) t.Run("a client with a certificate gets the stock", func(t *testing.T) { client := &Client{BaseURL: m.URL(), HTTP: clientTLS(t, true)} n, err := client.Stock(t.Context(), "sku-1") require.NoError(t, err) require.Equal(t, 7, n) }) t.Run("a client without one is refused", func(t *testing.T) { client := &Client{BaseURL: m.URL(), HTTP: clientTLS(t, false)} _, err := client.Stock(t.Context(), "sku-1") require.ErrorContains(t, err, "certificate required") }) } ``` ## The same mock from the CLI The `vlatte` command serves a mock file with the same certificates: ```yaml # testdata/vlatte/_mocks/stock.mock.yaml request: method: GET path: /stock/sku-1 response: status: 200 header: content-type: application/json body: '{"available": 7}' ``` ```sh vlatte --https --addr=127.0.0.1:8443 \ --tls-cert=testdata/localhost.pem --tls-key=testdata/localhost-key.pem --tls-ca=testdata/ca.pem curl --cacert testdata/ca.pem --cert testdata/client.pem --key testdata/client-key.pem \ https://localhost:8443/stock/sku-1 # {"available": 7} ``` In [Docker](https://vlatte.dev/run/docker.md), mount `testdata` and set `VLATTE_HTTPS=true`, `VLATTE_TLS_CERT`, `VLATTE_TLS_KEY` and `VLATTE_TLS_CA`: the image serves HTTPS on port `8443`. --- Source: https://vlatte.dev/guides/llm-streaming.md # Testing an LLM streaming client This guide tests a Go client of a chat API that streams its answers: it puts the answer together as the chunks arrive, fails when the stream drops midway, and fails on a rate limit. It uses [event streams](https://vlatte.dev/sse/index.md), [faults](https://vlatte.dev/http/faults.md) and [request matching](https://vlatte.dev/http/matching.md). ## The client `complete` asks for a completion with `"stream": true`, in the shape of OpenAI's chat completions API, and reads the answer from the `data` of each event until `[DONE]`: ```go // complete asks for a chat completion, streamed, and returns the answer once the stream sends [DONE]. func complete(ctx context.Context, client *http.Client, url, prompt string) (string, error) { body := fmt.Sprintf(`{"model": "gpt-test", "stream": true, "messages": [{"role": "user", "content": %q}]}`, prompt) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url+"/v1/chat/completions", strings.NewReader(body)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/event-stream") req.Header.Set("Authorization", "Bearer sk-test") res, err := client.Do(req) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return "", fmt.Errorf("status %d", res.StatusCode) } var answer strings.Builder lines := bufio.NewScanner(res.Body) for lines.Scan() { data, ok := strings.CutPrefix(lines.Text(), "data: ") if !ok { continue // a blank line between events, or another field } if data == "[DONE]" { return answer.String(), nil } var chunk struct { Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return "", err } for _, c := range chunk.Choices { answer.WriteString(c.Delta.Content) } } if err := lines.Err(); err != nil { return "", err } return "", errors.New("the stream ended before [DONE]") } ``` ## A streamed answer The mock matches the client's key and a request that asks to stream, and sends three chunks of JSON, 20 ms apart, then `[DONE]`. `chunk` builds each event, so the tests read as the stream does: ```go // chunk is an event of a streamed chat completion, with a piece of the answer. func chunk(content string) *mockhttp.Event { return mockhttp.NewEvent().DataJSON(map[string]any{ "object": "chat.completion.chunk", "choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": content}}}, }) } func TestPutsTheAnswerTogether(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(mockhttp.Postf("/v1/chat/completions"). Header("Authorization", match.Equal("Bearer sk-test")). Body(match.JSONPath("$.stream", match.Equal(true))). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( chunk("Hel"), chunk("lo, "), chunk("world"), mockhttp.NewEvent().Data("[DONE]"), ).EventDelay(20 * time.Millisecond)))) answer, err := complete(t.Context(), http.DefaultClient, m.URL(), "Say hello") if err != nil { t.Fatal(err) } if answer != "Hello, world" { t.Fatalf("answer = %q", answer) } scoped.AssertCalled(t) } ``` Each chunk goes as one `data:` line of compact JSON, flushed as it is sent, so the client reads it while the next one waits: ```text data: {"choices":[{"delta":{"content":"Hel"},"index":0}],"object":"chat.completion.chunk"} ``` A request with `"stream": false` doesn't match this mock. To test a client that asks for both, add a mock that matches `false` and answers the whole completion as JSON. ## A stream that drops A connection can drop in the middle of an answer. `fault.LimitData(100)` sends the first 100 bytes of the stream, the first chunk and part of the second, then closes the connection. A client that returns what it got so far would hand on half an answer: `complete` must fail. ```go func TestFailsWhenTheStreamDrops(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockhttp.Postf("/v1/chat/completions"). Pipe(fault.LimitData(100)). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( chunk("Hel"), chunk("lo, "), chunk("world"), mockhttp.NewEvent().Data("[DONE]"), )))) answer, err := complete(t.Context(), http.DefaultClient, m.URL(), "Say hello") if !errors.Is(err, io.ErrUnexpectedEOF) { t.Fatalf("err = %v, answer = %q", err, answer) } } ``` ## A rate limit An API answers `429` before any stream starts, with a JSON error. The client must not read it as an empty stream: ```go func TestFailsOnARateLimit(t *testing.T) { m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) m.MustMock(mockhttp.Postf("/v1/chat/completions"). Reply(mockhttp.Status(http.StatusTooManyRequests). Header("Retry-After", "1"). JSON(map[string]any{"error": map[string]any{"type": "rate_limit_error", "message": "slow down"}}))) _, err := complete(t.Context(), http.DefaultClient, m.URL(), "Say hello") if err == nil || err.Error() != "status 429" { t.Fatalf("err = %v", err) } } ``` ## The same mock in a file A [mock file](https://vlatte.dev/http/mock-files.md) keeps the stream with the tests' other mocks, and the [CLI](https://vlatte.dev/run/cli.md) serves it to a client that isn't in Go. In `data`, an object is sent as JSON, and text as it is: ```yaml request: method: POST path: /v1/chat/completions header: authorization: "Bearer sk-test" body: [jsonpath, $.stream, [equal, true]] response: status: 200 event_stream: event_delay: 20ms events: - data: object: chat.completion.chunk choices: - index: 0 delta: content: "Hel" - data: object: chat.completion.chunk choices: - index: 0 delta: content: "lo, " - data: object: chat.completion.chunk choices: - index: 0 delta: content: "world" - data: "[DONE]" ``` ## Named events Anthropic's Messages API names each event after the type of its data, and sends pings between them. A client of it dispatches on the name: it builds the text from `content_block_delta` events, skips `ping`, and stops at `message_stop`. `event` names each event: ```yaml request: method: POST path: /v1/messages header: x-api-key: test-key anthropic-version: "2023-06-01" body: [jsonpath, $.stream, [equal, true]] response: status: 200 event_stream: events: - event: message_start data: type: message_start message: id: msg_1 role: assistant content: [] - event: content_block_start data: type: content_block_start index: 0 content_block: type: text text: "" - event: ping data: type: ping - event: content_block_delta data: type: content_block_delta index: 0 delta: type: text_delta text: Hello - event: content_block_delta data: type: content_block_delta index: 0 delta: type: text_delta text: ", world" - event: content_block_stop data: type: content_block_stop index: 0 - event: message_delta data: type: message_delta delta: stop_reason: end_turn - event: message_stop data: type: message_stop ``` A client that splits events at blank lines, and reads both `event:` and `data:` lines, gets `Hello, world` from it. --- Source: https://vlatte.dev/guides/mcp-server.md # Mocking a remote MCP server This guide mocks a remote [MCP](https://modelcontextprotocol.io) server, a weather server over Streamable HTTP, with mock files, and tests an MCP client against it: the client connects, calls a tool, gets its progress and its result. It uses [event streams](https://vlatte.dev/sse/index.md), [templates](https://vlatte.dev/templating/index.md) and [JSON matching](https://vlatte.dev/http/matching.md#json-and-xml-bodies). > **Tip** > > This is vlatte playing an MCP server that your code calls. For vlatte serving MCP to your AI agent, so the agent can drive the mock server, see [MCP](https://vlatte.dev/mcp/index.md). ## How Streamable HTTP talks An MCP client sends each JSON-RPC message as a `POST` to one endpoint, `/mcp` here. The server answers a request with JSON, or with an event stream that sends messages, such as progress, before the answer. It starts a session in its answer to `initialize`, with an `Mcp-Session-Id` header, which the client sends with every later request. The client also listens on a `GET` of the endpoint for what the server sends on its own, and ends the session with a `DELETE`. Each message is a mock that matches the method of the JSON-RPC body with `jsonpath`, so every mock gets its own requests at the one endpoint. ## The mock files `initialize` answers with an event stream. Its one event is the result, a template that takes the request's `id` and agrees to the protocol version the client asked for: ```yaml # testdata/mcp/initialize.yaml id: mcp-initialize request: method: POST path: /mcp header: accept: [contains, text/event-stream] body: [jsonpath, $.method, [equal, initialize]] response: status: 200 header: mcp-session-id: weather-1 event_stream: events: - event: message data: >- {{ $r := fromJSON .Request.Body.String }}{"jsonrpc": "2.0", "id": {{ $r.id | toJSON }}, "result": { "protocolVersion": {{ $r.params.protocolVersion | toJSON }}, "capabilities": {"tools": {"listChanged": true}}, "serverInfo": {"name": "weather", "version": "1.0.0"}}} template: enabled: true ``` A notification has no answer but `202 Accepted`: ```yaml # testdata/mcp/initialized.yaml id: mcp-initialized request: method: POST path: /mcp header: mcp-session-id: weather-1 body: [jsonpath, $.method, [equal, notifications/initialized]] response: status: 202 ``` The Go SDK's client tries `server/discover` before `initialize`. A server built before stateless MCP doesn't know it, and answers with an error, which makes the client fall back to `initialize`: ```yaml # testdata/mcp/discover.yaml id: mcp-discover request: method: POST path: /mcp body: [jsonpath, $.method, [equal, server/discover]] response: status: 200 header: content-type: application/json body: '{"jsonrpc": "2.0", "id": {{ (fromJSON .Request.Body.String).id | toJSON }}, "error": {"code": -32601, "message": "Method not found"}}' template: enabled: true ``` `tools/list` lists one tool, and `tools/call` streams its progress, then its result, on the answer to the call. Both match the session, and the call matches the tool's name too: ```yaml # testdata/mcp/tools_list.yaml id: mcp-tools-list request: method: POST path: /mcp header: mcp-session-id: weather-1 body: [jsonpath, $.method, [equal, tools/list]] response: status: 200 event_stream: events: - event: message data: >- {"jsonrpc": "2.0", "id": {{ (fromJSON .Request.Body.String).id | toJSON }}, "result": {"tools": [{ "name": "get_weather", "description": "Current weather of a city", "inputSchema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}]}} template: enabled: true ``` ```yaml # testdata/mcp/tools_call.yaml id: mcp-tools-call request: method: POST path: /mcp header: mcp-session-id: weather-1 body: [all, [[jsonpath, $.method, [equal, tools/call]], [jsonpath, $.params.name, [equal, get_weather]]]] response: status: 200 event_stream: events: - event: message data: >- {{ $r := fromJSON .Request.Body.String }}{"jsonrpc": "2.0", "method": "notifications/progress", "params": { "progressToken": {{ $r.params._meta.progressToken | toJSON }}, "progress": 1, "total": 2, "message": {{ printf "looking up %s" $r.params.arguments.city | toJSON }}}} - event: message data: >- {{ $r := fromJSON .Request.Body.String }}{"jsonrpc": "2.0", "id": {{ $r.id | toJSON }}, "result": { "content": [{"type": "text", "text": {{ printf "Sunny, 24°C in %s" $r.params.arguments.city | toJSON }}}]}} template: enabled: true ``` The `data` that holds a template is text, folded with `>-`: data written as a YAML object is sent as JSON, never as a template. See [Templated events](https://vlatte.dev/sse/events.md#templated-events). The `GET` the client listens on [keeps open](https://vlatte.dev/sse/timing.md#keeping-open) for the session, with a heartbeat, and tells the client the tools changed. The `DELETE` ends the session: ```yaml # testdata/mcp/listen.yaml id: mcp-listen request: method: GET path: /mcp header: mcp-session-id: weather-1 last-event-id: [not, present] response: status: 200 event_stream: keep_open: true heartbeat: 15s events: - event: message data: jsonrpc: "2.0" method: notifications/tools/list_changed ``` ```yaml # testdata/mcp/delete.yaml id: mcp-delete request: method: DELETE path: /mcp header: mcp-session-id: weather-1 response: status: 204 ``` ## The test The test loads the mock files, and connects the [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk)'s client to the server, as an agent would. It calls the tool with a progress token, so the server's progress reaches the client's handler: ```go func TestCallsTheWeatherTool(t *testing.T) { files, err := filepath.Glob("testdata/mcp/*.yaml") if err != nil { t.Fatal(err) } builders := make([]vlatte.Builder[*mockhttp.Mock, *mockhttp.Server], len(files)) for i, f := range files { builders[i] = mockhttp.FromFile(f) } m := mockhttp.MustNew(mockhttp.Setup().Silent()) m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(builders...) progress := make(chan string, 1) client := mcp.NewClient(&mcp.Implementation{Name: "agent", Version: "v1"}, &mcp.ClientOptions{ ProgressNotificationHandler: func(_ context.Context, req *mcp.ProgressNotificationClientRequest) { progress <- req.Params.Message }, }) session, err := client.Connect(t.Context(), &mcp.StreamableClientTransport{Endpoint: m.URL("/mcp")}, nil) if err != nil { t.Fatal(err) } params := &mcp.CallToolParams{Name: "get_weather", Arguments: map[string]any{"city": "Lisbon"}} params.SetProgressToken("call-1") res, err := session.CallTool(t.Context(), params) if err != nil { t.Fatal(err) } if text := res.Content[0].(*mcp.TextContent).Text; text != "Sunny, 24°C in Lisbon" { t.Fatalf("text = %q", text) } if msg := <-progress; msg != "looking up Lisbon" { t.Fatalf("progress = %q", msg) } if err := session.Close(); err != nil { t.Fatal(err) } m.Close() // waits for the requests it serves, so every call is counted if hits := scoped.Mock("mcp-tools-call").Hits(); hits != 1 { t.Fatalf("tools/call hits = %d", hits) } } ``` A stream counts once it starts, but a mock that doesn't stream, such as `mcp-delete`, counts its request once it answered, which can be after the client read the answer: closing the server first waits for the requests it serves, so every count is final. The same files, served by the [CLI](https://vlatte.dev/run/cli.md), mock the server for an agent or a client in any language. ## A call the server closes A server may close the stream of a long call once it sent an event with an id, to free the connection. The client then reconnects with a `GET` that carries that id as its `Last-Event-ID`, and gets the answer there. Two more mocks play it, with the call's JSON-RPC id as the event id, so the mock that resumes knows what it answers: ```go m.MustMock( mockhttp.Postf("/mcp").ID("forecast-call"). Header("Mcp-Session-Id", match.Equal("weather-1")). Body(match.JSONPath("$.method", match.Equal("tools/call")), match.JSONPath("$.params.name", match.Equal("get_forecast"))). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().ID(`{{ (fromJSON .Request.Body.String).id }}`).Data(""), ).Retry(10*time.Millisecond).Template())), mockhttp.Getf("/mcp").ID("forecast-resume"). Header("Mcp-Session-Id", match.Equal("weather-1")). Header(mockhttp.HeaderLastEventID, match.Present()). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Name("message").Data(`{"jsonrpc": "2.0", "id": {{ .Request.Header.Get "Last-Event-ID" }}, `+ `"result": {"content": [{"type": "text", "text": "Rain tomorrow"}]}}`), ).Template())), ) ``` The first stream ends after its event, and its `retry` tells the client to reconnect 10 ms later. `listen.yaml` doesn't match the reconnection, as it requires no `Last-Event-ID`. See [Reconnecting](https://vlatte.dev/sse/reconnecting.md). --- Source: https://vlatte.dev/http/index.md # HTTP `mockhttp` runs HTTP mock servers for tests, and is the server the `vlatte` command runs. ```go func TestHello(t *testing.T) { m := mockhttp.MustNew() m.MustStart() t.Cleanup(m.Close) scoped := m.MustMock(mockhttp.Getf("/hello").Reply(mockhttp.OK().PlainText("hello world"))) res, err := http.Get(m.URL("/hello")) if err != nil { t.Fatal(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } if string(body) != "hello world" { t.Errorf("body = %q", body) } scoped.AssertCalled(t) } ``` `mockhttp.New` does the same as `MustNew` but returns an error instead of panicking. Pass `mockhttp.Setup()` to either one to configure the server: its address, TLS, CORS, proxying or recording, for example. ## In this section - [Mocks](https://vlatte.dev/http/mocks.md): building mocks and adding them to a server. - [Request matching](https://vlatte.dev/http/matching.md): what a mock can match in a request. - [Replies](https://vlatte.dev/http/replies.md): statuses, headers, bodies, sequences and functions. - [Forms and multipart](https://vlatte.dev/http/forms-multipart.md): matching uploads and replying with multipart bodies. - [SSE (Server-sent Events)](https://vlatte.dev/sse/index.md): replying with event streams. - [Proxying](https://vlatte.dev/http/proxying.md): proxy replies, proxy all, the forward proxy and HTTPS interception. - [Pipes](https://vlatte.dev/http/pipes.md) and [Faults](https://vlatte.dev/http/faults.md): changing, slowing and breaking answers. - [Recording](https://vlatte.dev/recording/http.md): recording a real service as mock files. - [Mock files](https://vlatte.dev/http/mock-files.md): the YAML and JSON format. - [TLS](https://vlatte.dev/http/tls.md) and [CORS](https://vlatte.dev/http/cors.md). - [Troubleshooting](https://vlatte.dev/http/troubleshooting.md): failures, near misses and debug headers. What HTTP shares with gRPC, such as [matchers](https://vlatte.dev/matchers/index.md), [scenarios](https://vlatte.dev/scenarios/index.md), [templates](https://vlatte.dev/templating/index.md), [webhooks](https://vlatte.dev/webhooks/index.md) and the [journal](https://vlatte.dev/more/journal.md), works the same for both: each has a section of its own in the sidebar. --- Source: https://vlatte.dev/http/mocks.md # Mocks A mock is built with a `*mockhttp.Builder` and added to a server with `m.Mock` or `m.MustMock`, which return a scope of the mocks they added. ```go scoped := m.MustMock( mockhttp.Getf("/users/%d", 1).Reply(mockhttp.OK().JSON(map[string]any{"id": 1})), mockhttp.Postf("/users").Reply(mockhttp.Created()), ) // ... the code under test calls the server ... scoped.AssertCalled(t) ``` ## Starting a mock | Function | Starts a mock that matches | |---|---| | `Getf(path, a...)`, `Postf`, `Putf`, `Patchf`, `Deletef`, `Headf` | That method, and a path glob formatted as `fmt.Sprintf` formats it. | | `Get(m)`, `Post(m)`, `Put(m)`, `Patch(m)`, `Delete(m)`, `Head(m)` | That method, and a URL matcher, such as `match.URLPath("/users")`. | | `AnyMethod()` | Any method, and any path. | | `Request()` | Nothing yet: add matchers yourself. Also the request part of a [record filter](https://vlatte.dev/recording/index.md) and of a [journal query](https://vlatte.dev/more/journal.md). | | `ProxyAll(From(target))` | What no other mock matches: see [Proxying](https://vlatte.dev/http/proxying.md#proxy-all). | | `FromFile(path)`, `FromBytes(data, format)` | What a [mock file](https://vlatte.dev/http/mock-files.md) says. | Then chain [matchers](https://vlatte.dev/http/matching.md), a [reply](https://vlatte.dev/http/replies.md), and the mock features below. ## Mock features | Method | What it does | |---|---| | `ID(id)`, `Name(name)` | Name the mock, in the admin API, the logs and the debug headers. | | `Priority(p)` | When several mocks match, the lowest number answers. | | `Enabled(false)` | Start the mock disabled: its scope's `Enable`, or the admin API, enables it. | | `Times(n)`, `Once()` | Serve a number of requests, then stop matching. See [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md). | | `Delay(d)`, `DelayFunc(delay)` | Wait before replying. | | `StartScenario`, `ScenarioIs`, `ScenarioStateIs`, `ScenarioStateWillBe`, `ScenarioKey` | Take part in a [scenario](https://vlatte.dev/scenarios/index.md). | | `OnCompleteEvent(cfg)`, `Callback(fn)` | Run [events such as webhooks](https://vlatte.dev/webhooks/index.md), or a function, once the mock replied. | | `Map(mapper)` | Change the response after it was built. A mapper finds the body in `Body`: one that streams, such as a proxy's, is read whole for it first. | | `Pipe`, `WirePipe`, `UpstreamPipe` | Pass the answer through [pipes](https://vlatte.dev/http/pipes.md), such as [faults](https://vlatte.dev/http/faults.md). | ## Adding, reloading and removing - `m.Mock(builders...)` adds mocks and returns their scope, or an error when one fails to build. `m.MustMock` panics instead. - `m.Unmock(ctx, scope)` removes the mocks of a scope, `m.Remock(ctx, old, builders...)` replaces them, and `m.Clean(ctx)` removes every mock. - A server loads [mock files](https://vlatte.dev/http/mock-files.md) on start. `m.Reload()` replaces the mocks of the files with what they hold now, in one step. When a file has problems, it returns them and the mocks stay as they were. - The [admin API](https://vlatte.dev/admin-api/index.md) adds, replaces, enables, disables and removes mocks while the server runs. ## Asserting A scope's assertions check how its mocks were called, and the server's how all of its mocks were: | Method | Checks | |---|---| | `AssertCalled(t)`, `AssertNotCalled(t)` | Every mock was called, or none was. | | `AssertNumberOfCalls(t, n)` | The mocks were called `n` times in all. | | `Called()`, `Hits()`, `Pending()` | The same, as values: whether they were called, how many times, and whether some wasn't. | | `ListCalled()`, `ListPending()` | The mocks that were called, and those that weren't. | To check the requests themselves, such as their headers or bodies, keep a [request journal](https://vlatte.dev/more/journal.md). --- Source: https://vlatte.dev/http/matching.md # Request matching A mock matches a request when every matcher it has matches. Each part of the request has a builder method in Go and a key under `request` in a [mock file](https://vlatte.dev/http/mock-files.md). The matchers themselves, such as `match.Equal` or `[contains, x]`, are shared by every protocol: see [Matchers](https://vlatte.dev/matchers/index.md). ## What a mock can match | Go | Mock file | Matches | |---|---|---| | `Method(methods...)`, `MethodMatches(m)` | `method` | The method, ignoring case: one of several, such as `method: get, post`. | | `Scheme(s)`, `SchemeMatches(m)` | `scheme` | `http` or `https`. | | `Host(m)` | `host` | The host the request names, without its port. Text ignores case. | | `URL(m)`, `URLf(format, a...)` | `url`, `url_match` | The URL. See [URLs](#urls). | | `RequestURI(uri)` | `url` (relative) | The path and query, exactly. | | `URLPath(m)`, `URLPathf(format, a...)` | `path` | The path. `URLPathf` and `Getf` take a glob. | | `URLPathTemplate(t)`, `PathParam(name, m)` | `path_template`, `path_params` | A path of a shape, and its parameters. See [Path templates](#path-templates). | | `URLFragment(m)` | `fragment` | The decoded text after a `#` in the request target. | | `Query(name, m)`, `Queryf`, `Queries(name, m)` | `query`, `queries` | Query parameters. See [Many values](#many-values). | | `Header(name, m)`, `Headerf`, `Headers(name, m)` | `header`, `headers` | Headers, by name in any case. | | `ContentType(value)` | `content-type` under `header` | The `Content-Type`, ignoring case. A value without parameters matches the media type alone. | | `Cookie(name, m)` | `cookie` | Cookies by name. | | `BasicAuth(user, pass)`, `BasicAuthMatches(user, pass)` | `basic_auth` | Basic credentials. | | `ClientIP(m)` | `client_ip` | The client's address, as the server's connection has it, not `X-Forwarded-For`. | | `FormField`, `FormFields`, `FormFile` | `form`, `forms`, `form_file` | Form fields and files: see [Forms and multipart](https://vlatte.dev/http/forms-multipart.md). | | `Body(matchers...)` | `body` | The body. See [JSON and XML bodies](#json-and-xml-bodies). | | `Matches(m)`, `RequestMatches(fn)` | `custom` | The whole `*http.Request`. See [Custom matchers](https://vlatte.dev/matchers/custom.md). | ```go m.MustMock(mockhttp.Postf("/pets"). Header("Authorization", match.HasPrefix("Bearer ")). Query("dry_run", match.Equal(true)). Body(match.JSONPath("$.name", match.Equal("Rex"))). Reply(mockhttp.Created())) ``` ```yaml request: method: POST path: /pets header: Authorization: [hasPrefix, "Bearer "] query: dry_run: [equal, true] body: [jsonpath, $.name, [equal, Rex]] ``` ## Text and types Most of a request is text: its path, query, headers and form values. A matcher reads that text as the type it compares with: - `[equal, 10]` and `[isIn, [10, 20]]` match `?page=10`; - `[gt, 18]` matches `?age=30`; - `[equal, true]` matches `?debug=1`. No other reading happens: `[equal, 10]` doesn't match `010`. **Keys** are kept as written, so a query name like `userId` matches `userId` only. Header names, and methods written as text, ignore case, so `method: get, post` matches `POST`; cookie names don't. ## URLs `url: /items?page=2` matches that path and query exactly: the same parameters, in any order, each with its values in order, so `/items` and `/items?page=2&sort=asc` don't match. Path segments compare decoded, so `%41` is `A`, but `%2F` isn't `/`. A relative `url` with a `*` in its path is an error: match a path with a glob through `path: /items/*`, and parameters one by one through `query`. For a literal `*`, write `%2A`. An absolute `url`, such as `https://api.example.com/items`, compares as text ignoring case, with the server's URL, or the one a request names in absolute form, as the [forward proxy](https://vlatte.dev/http/proxying.md#forward-proxy) gets it, or intercepted, without its default port. In Go, `RequestURI("/items?page=2")` matches as a relative `url` does. ## Path templates `path_template: /users/{id}/orders/{orderId}` matches paths of its shape: as many segments, the literal ones the same once decoded, and a non-empty one for each parameter, so `/users/7/orders` and `/users/7/orders/9/` don't match. A parameter is a whole segment; write a literal brace as `%7B` or `%7D`. `path_params` matches the parameters by name, decoded, and [templates](https://vlatte.dev/templating/index.md) read them as `.Request.PathParams.id`, or `index .Request.PathParams "order-id"` for a name that isn't a Go identifier. `url`, `url_match` or `path` can match the request too. In Go, `URLPathTemplate` and `PathParam`. ```yaml request: method: GET path_template: /users/{id} path_params: id: [regex, '^[0-9]+$'] response: body: 'user {{ .Request.PathParams.id }}' template: enabled: true ``` ## Many values `query`, `header` and `form` read the first value of a name. `queries`, `headers` and `forms` (`Queries`, `Headers` and `FormFields` in Go) read every value, as a list, so they take matcher lists, such as `[contains, a]`, `[itemsMatch, [a, b]]` or `[len, 2]`: they refuse text, which never equals a list. A name the request doesn't have is an empty list, which `[not, present]` matches, and a header sent empty isn't. A header line such as `a, b` is one value. ## More of the request - `cookie` matches cookies by name, as `Cookie` does; a cookie the request doesn't have is null, which no text matches. - `basic_auth` matches Basic credentials, as `BasicAuth` does, and `BasicAuthMatches` takes matchers; without credentials, the username and password are null, which no text matches. A mock file names its credentials as follows: ```yaml request: basic_auth: username: jeff password: s3cret ``` - `host` matches the host the request names, without its port, and text ignores case: `Host` in Go. - `client_ip` matches the client's address, as the server's connection has it, not `X-Forwarded-For`: `ClientIP` in Go. ## JSON and XML bodies `[eqj, {...}]` matches a JSON body equal to a document, `[jsonschema, schema]` one that conforms to a JSON Schema, `[eqx, '...']` an equal XML document, and `[jsonpath, ...]` and `[xpath, ...]` match what a path selects. Their options, such as `ignore_array_order`, are in [JSON and XML](https://vlatte.dev/matchers/json-xml.md). ```yaml request: method: POST path: /pets body: [all, [ [eqj, {name: Rex, tags: [a, b]}, {ignore_array_order: true, ignore_extra_elements: true}], [jsonpath, $.id, [regex, '^[0-9]+$']] ]] ``` ## Binary bodies `[equalbytes, "//4A"]` matches a body whose bytes are those of the standard base64 given, whose line breaks are ignored, exactly: no text reads as a number. In Go, `EqualBytes(b)`. `Equal(b)` matches the same bytes, but a mock's description, which the admin API serves, writes them as base64 text, which loads back as `[eq, ""]`, another matcher: `EqualBytes` describes them as `[equalbytes, ...]`, so the mock loads back. ## Request bodies Go's `net/http` parses request bodies by their media type, in any case: `text/plain` and `application/json` as text, and the two form types as forms. `Setup().MaxBodyParsingLimit(n)` bounds how much of a body the server reads to match it, `Setup().NoBodyParsing()` reads none, and `Setup().RequestBodyParsers(...)` adds parsers of your own. --- Source: https://vlatte.dev/http/replies.md # Replies A mock's reply says how it answers. In Go, `Reply(...)` takes a reply; in a [mock file](https://vlatte.dev/http/mock-files.md), `response` describes one, and `response_sequence`, `response_random`, `proxy` and `proxy_all` the others. ## Status, headers and body A `*mockhttp.Reply` starts from its status: `OK()`, `Created()`, `Accepted()`, `NoContent()`, `BadRequest()`, `Unauthorized()`, `Forbidden()`, `NotFound()`, `InternalServerError()` and the others, `Status(code)` for any code, and `NewReply()`. ```go m.MustMock(mockhttp.Getf("/users/1").Reply(mockhttp.OK(). Header("Cache-Control", "no-store"). JSON(map[string]any{"id": 1, "name": "ann"}))) ``` ```yaml request: method: GET path: /users/1 response: status: 200 header: cache-control: no-store body: id: 1 name: ann ``` | Go | Mock file | Sets | |---|---|---| | `Status(code)` | `status` | The status. | | `Header(k, v)`, `HeaderArr(k, vs...)` | `header` | Headers. A value can be a list. | | `HeaderTemplate(k, v)`, `HeaderArrTemplate` | `header_template` | Headers rendered from the request: see [Templates](https://vlatte.dev/templating/index.md). | | `Trailer(k, v)`, `TrailerArr(k, vs...)` | `trailer` | Trailers. | | `Cookie(c)`, `ExpireCookie(c)` | | Cookies, and cookies that expire one the client holds. | | `ContentType(mime)` | `content-type` under `header` | The `Content-Type`. | | `PlainText(text)`, `JSON(v)` | | A `text/plain` or `application/json` body. | | `BodyText(s)`, `Body(b)`, `BodyJSON(v)`, `BodyReader(r)` | `body`, `body_base64` | The body. A mock file's `body` is text as is, and any other value as JSON. | | `BodyFile(name)`, `BodyFileWithTemplate(name)` | `body_file` | A body read from a file, relative to the mock file. | | `BodyTemplate(text)` | `body` with `template.enabled` | A body rendered from the request. | | `Problem(p)` | | An [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document, built with `NewProblem()`. | | `Multipart(mp)` | `multipart` | A multipart body: see [Forms and multipart](https://vlatte.dev/http/forms-multipart.md#multipart-replies). | | `EventStream(es)` | `event_stream` | A `text/event-stream` body, sent event by event: see [SSE (Server-sent Events)](https://vlatte.dev/sse/index.md). | | `Gzip()` | `encoding: gzip` | A gzip-encoded body. | A body that isn't UTF-8 text, such as an image or text in another encoding, goes in `body_base64`, as standard base64 (`body_base64: //4A`), whose line breaks are ignored, or in a `body_file`. A reply takes one of `body`, `body_base64`, `body_file`, `multipart` and `event_stream` at most. ```go m.MustMock(mockhttp.Postf("/orders").Reply(mockhttp.UnprocessableEntity().Problem(mockhttp.NewProblem(). Title("Out of stock"). Detail("item 42 is out of stock"). Extension("item", 42)))) ``` ## Sequences `Seq(replies...)` answers each request with the next reply, in order, then the reply of `OnSequenceEnded`. A request past the end without one fails, with `VLatte-Error: reply`. Each mock built with a sequence runs it on its own, from the first reply. ```go m.MustMock(mockhttp.Getf("/jobs/1").Reply(mockhttp.Seq( mockhttp.OK().JSON(map[string]any{"status": "running"}), mockhttp.OK().JSON(map[string]any{"status": "running"}), ).OnSequenceEnded(mockhttp.OK().JSON(map[string]any{"status": "done"})))) ``` ```yaml request: method: GET path: /jobs/1 response_sequence: responses: - status: 200 body: status: running - status: 200 body: status: running after_ended: status: 200 body: status: done ``` ## Random replies `Rand(replies...)` answers each request with one of its replies at random. `RandWithSeed(seed, replies...)` picks in an order the seed repeats, and `RandWith(r, replies...)` with a `*rand.Rand` of yours. In a mock file, `response_random` takes `responses` and `seed`. ```yaml request: method: GET path: /flaky response_random: seed: 42 responses: - status: 200 - status: 503 ``` ## Proxy replies `From(target)` answers with what another server answers, `proxy` in a mock file. See [Proxying](https://vlatte.dev/http/proxying.md). ## Replies in Go only Mock files can't hold these, so the admin API can't show, save or store a mock that uses them. | Reply | Answers | |---|---| | `Echo()` | The request back, as it came. `Log()` logs each. | | `Func(fn)` | What `fn` returns, a `*mockhttp.Response`, or what it writes to the `http.ResponseWriter` when it returns `nil`. | | `Handler(h)` | What an `http.HandlerFunc` writes. | | `Shared(replier)` | The same `Replier` for every mock built with it, which they never close. | ```go m.MustMock(mockhttp.Postf("/echo").Reply(mockhttp.Echo())) m.MustMock(mockhttp.Getf("/now").Reply(mockhttp.Func(func(w http.ResponseWriter, r *mockhttp.RequestValues) (*mockhttp.Response, error) { return &mockhttp.Response{StatusCode: http.StatusOK, Body: []byte(time.Now().Format(time.RFC3339))}, nil }))) ``` The context of the request of a `Func` or `Handler` reply also ends when a [pipe](https://vlatte.dev/http/pipes.md) of the mock cuts the answer. --- Source: https://vlatte.dev/http/forms-multipart.md # Forms and multipart Go's `net/http` parses request bodies: `ParseForm` reads urlencoded forms and `ParseMultipartForm` reads `multipart/form-data`. Bodies are parsed by their media type, in any case: `text/plain` and `application/json` as text, and the two form types as forms. ## Fields `FormField` (file key `form`) reads a field of either encoding, as `http.Request.PostFormValue` does: its first value, and never the query. `FormFields` (file key `forms`) reads every value. ## Files `FormFile` (file key `form_file`) matches the files of a field with a pattern. Some file of the field must meet every condition of the pattern, or every file with `Every()` (`every: true`). With several patterns for one field, each must be met by some file of the field, and one file may meet several. ```go m.MustMock(mockhttp.Postf("/upload"). FormField("title", match.Equal("Holiday")). FormFile("avatar", mockhttp.FileWith(). Filename(match.HasSuffix(".png")). ContentType(match.Equal("image/png"))). Reply(mockhttp.Created())) ``` ```yaml request: method: POST path: /upload form: title: Holiday form_file: avatar: filename: [hasSuffix, .png] content_type: image/png # the media type, in lower case, without parameters body_file: avatar.png # the file's bytes; or body: text or a matcher list ``` Go's rules apply: - a part without a field name isn't read; - a file's name is its base: `dir/a.png` arrives as `a.png`; - a text field keeps no headers; - a form holds at most 1000 parts, which `GODEBUG=multipartmaxparts` changes. A form body that doesn't parse, or that was cut short, has no fields at all, and the server logs a warning. Other multipart requests, such as `multipart/mixed`, aren't parsed: match them with `Body`. ## Multipart replies `Reply.Multipart` writes a body of any multipart subtype with Go's `multipart.Writer`. The body sets the `Content-Type`, with its boundary. When none is set, a random boundary is drawn once per mock, so every response is the same bytes. ```go m.MustMock(mockhttp.Getf("/report").Reply(mockhttp.OK().Multipart(mockhttp.NewMultipart("related", mockhttp.NewPart().Header("Content-Id", "").BodyJSON(map[string]any{"id": 1}), mockhttp.NewPart().Name("report").Filename("report.pdf").BodyFile("testdata/report.pdf"), ).Param("start", "")))) ``` ```yaml response: status: 200 multipart: type: related # the subtype: form-data, mixed, related, x-mixed-replace… params: start: "" parts: - header: content-id: "" body: id: 1 # text as is, anything else as JSON - name: report filename: report.pdf body_file: report.pdf # relative to the mock file ``` - **Bodies.** A part takes one of `body`, `body_base64` and `body_file` at most. A body that isn't UTF-8 text, such as an image, goes in `body_base64`, as standard base64 (`body_base64: //4A`), whose line breaks are ignored, or in a `body_file`. A byte body set in Go with `Part.Body` describes in `body_base64` when it isn't UTF-8, or when the body is templated, so it loads back as bytes, not as a template. - **Defaults.** A part with a name or a filename gets its `Content-Disposition`. A JSON body defaults to `application/json`, and a part with a filename to `application/octet-stream`. - **Templates.** With `template.enabled`, or `Template()` in Go, the text of the parts is a template: names, filenames, content types, header values, text bodies and body file names. Byte bodies, such as `body_base64`, JSON bodies and the content of body files are written as they are. - **Encode.** `Multipart.Encode` writes the same bodies for requests in tests. ## Streaming A part with a `delay` waits that long before it is written, and everything before it is flushed first. A client therefore reads each part as it comes, as `multipart/x-mixed-replace` camera feeds need. ```yaml response: status: 200 multipart: type: x-mixed-replace parts: - content_type: image/jpeg body_file: frame1.jpg - content_type: image/jpeg body_file: frame2.jpg delay: 500ms ``` **Webhooks** send multipart bodies too: - the `multipart` parameter takes the same object, and `multipart_template` makes its text a template; - Go uses `webhook.Options().Multipart(mockhttp.MultipartForm(...))`; - a webhook sends its body at once, so its parts can't have delays; - relative body files open from the mock file's directory, as in replies: a whole `body_file` and a part's alike. **Templates** read forms: `{{ .Request.PostForm.Get "title" }}` and `{{ with .Request.FormFile "avatar" }}{{ .Filename }}{{ end }}`. **Recording** matches a recorded upload by all its text fields, each with its values in order, and its files by `form_file` patterns, so it replays whatever boundary the next client picks. Files that aren't UTF-8 text go to files next to the recording. An upload with another field doesn't match; one with more files still does. See [Recording and playback](https://vlatte.dev/recording/index.md). --- Source: https://vlatte.dev/http/proxying.md # Proxying vlatte sends requests on to real servers in three ways: - a **proxy reply**: a mock that answers with what its target answers, `From(target)` in Go and the `proxy` key in mock files; - the **forward proxy**: clients use the server as their HTTP proxy, `Setup().Proxy()` in Go and the `proxy` setting, and what no mock matches goes to its host; - the **recording target**: see [Recording and playback](https://vlatte.dev/recording/index.md). ## How proxies answer The three share one engine, and answer alike: - The body streams as the upstream sends it, trailers included. A body of unknown length, or an event stream, reaches the client as it comes. - The hop-by-hop headers of either connection, such as `Connection`, `Keep-Alive` and `Proxy-Authorization`, don't go on. A request's `Te: trailers` does. - The upstream's `VLatte-*` headers, such as the debug headers of an upstream that is a vlatte server too, don't reach the client: it gets the proxy's. - Compressed bodies go on as they came, and a redirect goes back to the client: the proxy doesn't follow it. - A request that asks to switch protocols, such as a WebSocket handshake (`Connection: Upgrade`, `Upgrade: websocket`), asks the upstream too. Once the upstream switches, the client gets its `101`, and the two connections are piped until both sides end, or the server closes, while the mock's callbacks and events run. A mock counts the call, and the log shows it, when the switch is done. A mock's pipes apply to the connection too: see [Pipes](https://vlatte.dev/http/pipes.md). A mock with response mappers doesn't ask the upstream to switch, as they can't apply to a connection: the upstream answers as to any request. `h2c`, which asks for HTTP/2 on the client's own connection, isn't asked on. An upstream that switches to a protocol the client didn't offer fails the call. A switch needs HTTP/1.1. - The timeout bounds the wait for the response headers, connecting and sending the request included: 30 seconds by default, and 0 waits forever. The body has no time limit. - A call that fails gets the server's answer to a failure, `ErrorStatusCode` with `VLatte-Error: reply`, unless the client left: then the log warns. Errors and logs show the upstream's URL without its password, and with its query values masked: `?api_key=xxxxx`. An upstream body that breaks off cuts the client's connection, so the client doesn't take part of it for all of it. - A request the proxy sends on carries the server's ID in `VLatte-Via`. A request that passes the same server 10 times, as in a loop, such as through a proxy reply whose target is the server itself, fails as a proxy loop rather than being sent on forever. A route may pass a server more than once, such as through a gateway that sends it back. - What the proxy keeps of a body, for the log, callbacks and events, is at most 1 MiB, and only when one of them reads it. - A request body the server read whole, shorter than `MaxBodyParsingLimit` and without `NoBodyParsing`, can be sent again: the transport resends a replayable request, such as one with an `Idempotency-Key`, on a new connection when the one it reused turned out closed. ## Proxy replies The target's path and query come before the request's: with `target: http://api.local/v1?tenant=a`, a request to `/users?page=2` goes to `http://api.local/v1/users?tenant=a&page=2`. `trim_prefix` and `trim_suffix` cut the request path first: `trim_prefix` cuts whole segments, so `/api` cuts `/api/users` to `/users` and leaves `/apiary` as it is, and `trim_suffix` cuts text, such as an extension: `.json` cuts `/users.json` to `/users`. When the target has a path, a request path with a `.` or `..` segment, escaped or not, fails: it could leave that path. `request_header` sets headers of the request the target gets, in place of the client's values of the same name, and `remove_request_headers` leaves request headers out; `header` sets headers of the response, in place of the target's. `x_forwarded: true` (`XForwarded()` in Go) tells the target about the client: its address in `X-Forwarded-For`, after those of the proxies before, the host it asked for in `X-Forwarded-Host`, and its scheme in `X-Forwarded-Proto`. `preserve_host: true` (`PreserveHost()`) sends the target the `Host` the client asked for, in place of the target's own. `timeout` is the timeout. A mock's `delay` comes before the call: a client that leaves during it gets nothing, and the target isn't called. A mock with response mappers reads the whole body first, so they see it in `Body`: up to 1 MiB, the most a proxy keeps of a body. A larger body fails the reply, and so does an event stream (`text/event-stream`) at once, as it doesn't end; a stream of another type that never ends waits for the client to leave. A mock's pipes can change the body's length: the target's `Content-Length` doesn't go on. A mock's description, which the admin API serves, keeps the target as it was set, credentials included, so it loads back as the same mock. The target's TLS certificate isn't verified unless `ssl_verify: true`, so a target with a self-signed certificate answers. Set it for a target that gets credentials, such as `https://user:pass@api.local`, whose user info goes as Basic auth: over `http`, it goes in cleartext. `ssl_verify` also decides whether the certificate of an `https` proxy on the way, from `HTTPS_PROXY`, is verified. ## Proxy all `ProxyAll(From(target))` in Go mocks what no other mock matches with a proxy reply: it matches any method and path, and comes last, at priority `math.MaxInt`. It leaves alone `CONNECT`s, and, on a server that is a forward proxy, the requests in absolute form, which the proxy sends on to their hosts. In a mock file, `proxy_all` takes the keys of `proxy` and makes the same mock, without `request`; a `ProxyAll` mock's description loads back as one. The `proxyall` recipe mocks it. The `proxy_all` setting does the same without a mock, so assertions don't count it, and `Clean` and `Reload` leave it: `vlatte --proxy-all-target=https://api.example.com`. It takes the keys of a mock file's `proxy` but `request_header` and `header`, which flags and variables can't give. ## Forward proxy A client sends its requests to the server in absolute form, `GET http://api.local/users`, and opens a tunnel with `CONNECT` for an `https` URL. A mock that matches the request answers it; mocks match the path and ignore the host, unless they match it, with `host` or an absolute `url`. A request no mock matches goes to its host. A request in origin form, `GET /users`, names no host: no mock matching it, it gets the no-match answer. `proxy.timeout` is the timeout, and `proxy.ssl_verify` verifies the certificates of the hosts, and of an `https` proxy on the way, `proxy.via` or the one of `HTTPS_PROXY`: none is verified by default. `proxy.via` sends the requests through another proxy, an `http`, `https`, `socks5` or `socks5h` URL whose user info goes as its credentials: a client's `Proxy-Authorization` is for vlatte, and doesn't go on. A `CONNECT` no mock matches opens a tunnel to its host, through `proxy.via` or the proxy of the environment, if any, within `proxy.timeout`, and the log shows that it opened. What goes through a tunnel, such as a TLS connection, isn't read: mocks don't see it, unless the proxy intercepts it. Each side of a tunnel ends what it sends on its own, and the tunnel closes once both did, or once the server closes. A tunnel needs HTTP/1.1, and a response writer that can hijack its connection: a middleware's writer must unwrap, with `Unwrap() http.ResponseWriter`, to the server's. ## HTTPS interception With `proxy.intercept` (`ProxyConfig{Intercept: true}` in Go), the proxy serves the TLS of a `CONNECT` no mock matches itself. It answers with a certificate for the host the `CONNECT` names, or the name the client asks for, signed by its CA, which clients must trust. The requests within are then the server's own, for `https://` that host, which `scheme` and an absolute `url` see: mocks answer them, recordings record them, the log shows them, and the proxy sends the rest on to the host. A connection that doesn't start with TLS goes to the host through a tunnel, as does one whose client offers only `h2` by ALPN, such as a gRPC client, and one whose client sends nothing for 2 seconds, waiting for a host that speaks first, such as an SSH server: a host the proxy can't reach then closes the connection, as the `200` went out already. `proxy.ca_cert` and `proxy.ca_key` name the CA's files, which the settings need: a CA's certificate, or an intermediate CA's, followed by its chain, which the proxy sends with each host's. In Go, `ProxyConfig.CA` sets it, and without one, the server makes a CA of its own, valid for a year, which `m.ProxyCA()` returns for clients to trust: ```go m := mockhttp.MustNew(mockhttp.Setup().Proxy(&mockhttp.ProxyConfig{Intercept: true})) pool := x509.NewCertPool() pool.AddCert(m.ProxyCA()) client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL), TLSClientConfig: &tls.Config{RootCAs: pool}}} ``` An intercepted connection speaks HTTP/1.1. ## Transports In Go, `From(target).Transport(rt)` sends a proxy reply through a transport of yours, such as one with a client certificate, and `Setup().Transport(rt)` sets one for every proxy reply, the recording target, the forward proxy and webhooks. A transport of yours replaces the one vlatte makes, and decides on its own how it verifies certificates and which proxy it goes through: `ssl_verify`, `proxy.via`, `proxy.ssl_verify` and `record.skip_ssl_verify` fail with it, rather than being ignored. The timeouts apply to any transport. The transports vlatte makes go through the proxy of `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`, which leaves out `localhost`, unless `proxy.via` sets one. Docker can set these variables in containers from `~/.docker/config.json`, and so send the hosts of a compose network, such as `http://api:8080`, to that proxy: add them to `NO_PROXY`. A variable that names the vlatte server itself sends the requests it proxies back to it, until they fail as a proxy loop: set `NO_PROXY=*` for its process, or `proxy.via`. ## Outbound rules `outbound.deny` lists address ranges the server never connects to, and `outbound.allow`, once set, the only ones it may: proxy replies, the recording target, the forward proxy and its tunnels, and webhooks. Deny wins over allow. A server that others reach, as in Docker, keeps them from using its mocks to reach hosts they shouldn't, such as cloud metadata at `169.254.169.254` or an internal network: ```sh vlatte --outbound-deny=169.254.0.0/16,10.0.0.0/8 ``` The ranges are in CIDR form, `VLATTE_OUTBOUND_DENY` sets them too, and in Go `mockhttp.Setup().OutboundDeny(netip.MustParsePrefix("169.254.0.0/16"))`. They check the address the server connects to, once a host name resolves, so a name that resolves into a refused range is refused, and either one refuses `0.0.0.0` and `::`, which reach the server's own host. An IPv4 range holds no IPv6 address: deny `::1/128` with `127.0.0.0/8`, and `fd00:ec2::254/128` with `169.254.0.0/16` on AWS. Through a proxy on the way, `proxy.via` or the one of the environment, they check that proxy's address. A refused connection fails with `vlatte.ErrOutboundRefused`, which the answer's body names. A transport of yours, `Setup().Transport(rt)` or `From(target).Transport(rt)`, dials on its own, and they don't apply to it. gRPC mocks have `grpc.outbound.allow` and `grpc.outbound.deny`, which connect proxy replies and the recording target directly once set, never through the proxy of the environment, and don't apply to a target on a unix socket. --- Source: https://vlatte.dev/http/pipes.md # Pipes Pipes are HTTP only, and Go only: in [mock files](https://vlatte.dev/http/mock-files.md), `pipes`, `wire_pipes` and `upstream_pipes` list [faults](https://vlatte.dev/http/faults.md#in-mock-files). In Go, `Pipe` passes the body of each answer of a mock through pipes, in order, before it reaches the client: to delay it, send it in parts, change it or cut it. A pipe is a `vlatte.Pipe`. For each answer, its `Open` makes the pipe's stage of that answer's `vlatte.Flow`, so a stage keeps what it counts for one answer only: a mock's answers don't share it. ```go upper := vlatte.PipeFunc(func(_ context.Context, b []byte, next vlatte.Stage) (int, error) { if _, err := next.Write(bytes.ToUpper(b)); err != nil { return 0, err } return len(b), nil }) m.MustMock(mockhttp.Getf("/hello").Pipe(upper).Reply(mockhttp.OK().PlainText("hello"))) ``` ## Stages A stage gets each write of the body, each flush, and the end of the answer, an empty body included. It can pass a write on as it is, changed, in parts, later or not at all, and send what it holds at a flush or at the end. It may write from a goroutine of its own, and needs no lock: vlatte orders its writes with the reply's. Pipes can change the body's length, so the answer has no `Content-Length`. A stage aborts the answer by returning an error that matches `vlatte.ErrAbort`, or with `Flow.Abort`, such as from a timer, which ends a reply that waits on the answer's context, as a proxy reply does. The mock still counts the call and runs its callbacks and events. Then the connection is cut, after what was sent: an HTTP/1 connection is closed, and an HTTP/2 stream is reset. With `vlatte.ErrReset`, an HTTP/1 connection is reset too, so the client reads `ECONNRESET`. A failure, such as a failing reply, is vlatte's answer, and skips the pipes. ## Wire pipes `WirePipe` passes every byte of each answer through its pipes, after those of `Pipe`: the status line, the headers, the framing of the body and the body, so a pipe can send what isn't HTTP, such as nothing at all, half the headers, or garbage after a valid head. The mock takes its connection over and answers in HTTP/1.x itself, with `Connection: close`: the body is chunked over HTTP/1.1, and ends with the connection over HTTP/1.0. A request over HTTP/2 can't give its connection over, so it gets vlatte's error answer: turn HTTP/2 off, or use an HTTP/1.1 client, to test a mock with wire pipes. An abort closes the connection without the rest of the answer, and `vlatte.ErrReset` resets it, once the mock counted the call and ran its callbacks and events. A recording leaves out the answers of mocks with wire pipes. ## Upstream pipes `UpstreamPipe` passes what a mock sends on through its pipes: the body of the request a proxy reply sends to its target. Pipes can change its length, so the target gets it chunked. The server reads the body of a request before it matches a mock, so these pipes don't slow the client's upload: they apply to what the mock sends on. A body the server doesn't read whole, with `NoBodyParsing` or past `MaxBodyParsingLimit`, is read from the client as the target reads it, a small piece at a time, so a pipe that slows it slows the upload too, once the connection's buffers are full. A mock with wire pipes takes its connection over before it replies, so it reads such a body through these pipes first: they pace the upload, and a proxy's target gets the body as they passed it on, at once. A stage that aborts ends the call to the target, and the client's answer is cut as for any abort. ## Switched protocols A connection that switched protocols, such as a WebSocket, goes through the pipes too. The answer ends with its head, so its pipes end there, and a stage that cuts it leaves the client without a switch. Then what the target sends the client passes the mock's pipes again, in a flow of its own, then its wire pipes, which saw the `101` too. What the client sends the target passes its upstream pipes. A stage that aborts either way, even on a timer while nothing comes, cuts both sides, or resets them with `vlatte.ErrReset`. --- Source: https://vlatte.dev/http/faults.md # Faults The `fault` package has pipes that make an HTTP mock answer wrong, to test how a client handles a server that fails, or a bad network. A pipe of either level acts on the body with `Pipe`, and on every byte of the answer, its head included, with `WirePipe`: ## Fault pipes | Pipe | Level | What the client gets | |---|---|---| | `fault.EmptyResponse()` | `WirePipe` | No byte at all: the connection ends where it waits for a status line. | | `fault.MalformedResponseChunk()` | `WirePipe` | The status line and headers, with the mock's status, then garbage in place of the first chunk of the body, then the end of the connection. | | `fault.RandomDataThenClose()` | `WirePipe` | `fault.RandomDataSize` random bytes in place of the answer, then the end of the connection. | | `fault.ConnectionResetByPeer()` | `WirePipe` | A TCP reset, such as `ECONNRESET`, before a status line. | | `fault.ChunkedDribbleDelay(chunks, total)` | `Pipe` | The whole body, in `chunks` flushed parts spread over `total`: part k comes at k·total/chunks, and the body ends at `total`. The body is held until the reply wrote all of it. | | `fault.Latency(latency, jitter)` | Either | Every write and flush `latency` later, give or take `jitter` at random, in order. The answer keeps its pace: the writer doesn't wait. | | `fault.Bandwidth(bytesPerSecond)` | Either | Each write at that rate, in flushed pieces of a tenth of a second. `fault.Bandwidth3G` and the other `fault.Bandwidth*` constants are common links, in bytes per second. | | `fault.Slicer(averageSize, sizeVariation, delay)` | Either | Each write in flushed slices of `averageSize` bytes, give or take `sizeVariation` at random, `delay` apart. | | `fault.SlowClose(delay)` | Either | The whole answer, then its end `delay` later: the body's last chunk with `Pipe`, the close of the connection with `WirePipe`. | | `fault.Timeout(timeout)` | Either | Nothing, then the end of the connection `timeout` after the answer began; with `Pipe`, the head comes with that end. A timeout of 0 never ends. | | `fault.LimitData(n)` | Either | The first `n` bytes, then the end of the connection: bytes of the body with `Pipe`, of the whole answer with `WirePipe`. | | `fault.ConnectionResetByPeerAfter(d)` | Either | The answer as it comes, then a TCP reset `d` after it began. With `Pipe` over HTTP/2, the stream is cut instead. | ```go m.MustMock(mockhttp.Getf("/users").WirePipe(fault.ConnectionResetByPeer()).Reply(mockhttp.OK())) m.MustMock(mockhttp.Getf("/report").Pipe(fault.ChunkedDribbleDelay(5, 2*time.Second)).Reply(mockhttp.OK().BodyFile("report.csv"))) m.MustMock(mockhttp.Getf("/search").WirePipe(fault.Latency(300*time.Millisecond, 60*time.Millisecond)).Reply(mockhttp.OK())) m.MustMock(mockhttp.Getf("/video").Pipe(fault.Bandwidth(fault.Bandwidth3G)).Reply(mockhttp.OK().BodyFile("video.mp4"))) ``` A wire fault needs HTTP/1.x, as wire pipes do. Faults combine with other pipes: a fault on the wire comes after the pipes of `Pipe`. A mock that answers with a fault still counts the call, and runs its callbacks and events, before the connection ends. ## In mock files `pipes`, `wire_pipes` and `upstream_pipes` list faults for `Pipe`, `WirePipe` and `UpstreamPipe`. A fault is an object: `type` names it, after its function in snake case, and its other keys are its parameters, by name. Durations are whole milliseconds or a Go duration, such as `1.5s`. A parameter with a default can be left out. | `type` | Parameters | |---|---| | `empty_response`, `malformed_response_chunk`, `random_data_then_close`, `connection_reset_by_peer` | None. | | `chunked_dribble_delay` | `chunks`, `total` | | `latency` | `latency`; `jitter`, 0 by default | | `bandwidth` | `rate`: bytes per second, or a `fault.Bandwidth*` constant in snake case: `gprs`, `dial_up`, `2g`, `good_2g`, `3g`, `good_3g`, `dsl`, `4g`, `wifi` or `fiber` | | `slicer` | `average_size`; `size_variation` and `delay`, 0 by default | | `slow_close` | `delay` | | `timeout` | `after`, 0 by default: never | | `limit_data` | `bytes` | | `connection_reset_by_peer_after` | `after` | ```yaml request: method: GET path: /report response: status: 200 body_file: report.csv pipes: - type: bandwidth rate: 3g - type: connection_reset_by_peer_after after: 5s wire_pipes: - type: latency latency: 300ms jitter: 60ms ``` A fault with a missing parameter, a key it doesn't take, or a value its function refuses fails the file, with each problem at its key, such as `pipes[1].after`. `faultbuild.Build`, of package `fault/faultbuild`, builds a fault from its object in Go. A mock describes its faults the same way, so the admin API shows them and its store keeps them. A pipe of your own, such as a `vlatte.PipeFunc`, isn't in the description. --- Source: https://vlatte.dev/http/mock-files.md # Mock files Mocks can also live in YAML or JSON files (`.json`, `.yaml` or `.yml`). - **On start,** the server loads the files matching `testdata/vlatte/_mocks/*mock.yaml` and `testdata/vlatte/_mocks/*mock.yml`, and the [recordings](https://vlatte.dev/recording/index.md) in `testdata/vlatte/_mocks_recorded`, in name order: the folders of its [work directory](https://vlatte.dev/run/configuration.md#work-directory). Change the patterns with `Setup().MockFilePatterns(...)`. - **To load one file,** use `m.MustMock(mockhttp.FromFile(path))`. - **`m.Reload()`** replaces the mocks of the files with what they hold now, in one step. When a file has problems, it returns them and the mocks stay as they were. ```yaml # testdata/vlatte/_mocks/hello_mock.yaml id: hello # optional: names the mock, as in the admin API's /mocks/hello request: method: GET path: /hello response: status: 200 header: content-type: text/plain body_file: hello.txt # relative to this file ``` [`mockhttp/schema/http.schema.json`](https://vlatte.dev/reference/schemas.md) describes the format, so editors can validate mock files. Its address is `https://vlatte.dev/schemas/http.schema.json`: start a YAML mock file with ```yaml # yaml-language-server: $schema=https://vlatte.dev/schemas/http.schema.json ``` and editors that use the YAML language server, such as VS Code with the YAML extension, complete and check it. A JSON mock file names it in a `"$schema"` key. See [Mock file schemas](https://vlatte.dev/reference/schemas.md) for other editors. ## Keys | Key | What it holds | |---|---| | `id`, `name`, `priority`, `enabled` | The mock's [features](https://vlatte.dev/http/mocks.md#mock-features). | | `request` | The [matchers](https://vlatte.dev/http/matching.md). | | `response` | The [reply](https://vlatte.dev/http/replies.md). | | `response_sequence`, `response_random` | [Sequences](https://vlatte.dev/http/replies.md#sequences) and [random replies](https://vlatte.dev/http/replies.md#random-replies). | | `proxy`, `proxy_all` | [Proxy replies](https://vlatte.dev/http/proxying.md#proxy-replies) and [proxy all](https://vlatte.dev/http/proxying.md#proxy-all). | | `delay`, `repeat` | A delay before replying, and how many requests the mock serves: see [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md). | | `scenario` | A [scenario](https://vlatte.dev/scenarios/index.md). | | `on_complete` | [Events such as webhooks](https://vlatte.dev/webhooks/index.md). | | `pipes`, `wire_pipes`, `upstream_pipes` | [Faults](https://vlatte.dev/http/faults.md#in-mock-files). | | `ext` | Settings for the server's mock file handlers, `Setup().MockFileHandlers(...)` in Go. | ## Types Each key takes one type, as the schema says. Write text in quotes where a string goes, as in `name: "123"`, and numbers bare, as in `priority: 10`. Bools read the way Go's `strconv.ParseBool` reads them, so `true`, `"true"`, `t` and `1` are all true. A value of the wrong type is an error that says how to write it. ## Bodies A reply's `body` is text as is, and any other value as JSON. A body that isn't UTF-8 text, such as an image or text in another encoding, goes in `body_base64`, as standard base64 (`body_base64: //4A`), whose line breaks are ignored, or in a `body_file`. A reply takes one of `body`, `body_base64`, `body_file`, `multipart` and `event_stream` at most. ## Durations `delay`, `proxy.timeout`, the durations of [faults](https://vlatte.dev/http/faults.md) and the `delay` and `timeout` of a [webhook](https://vlatte.dev/webhooks/index.md) take whole milliseconds (`1500`) or a Go duration (`1.5s`). `"1500"` in quotes is neither. ## Matching The `request` key matches requests as the Go builder does: see [Request matching](https://vlatte.dev/http/matching.md) for every key, and [Matchers](https://vlatte.dev/matchers/index.md) for the matcher lists, such as `[contains, x]` or `[jsonpath, $.id, [gt, 1]]`, and [Custom matchers](https://vlatte.dev/matchers/custom.md) for matchers of your own, in `request.custom`. ## Proxy replies A `proxy` reply (`From` in Go) answers with what its target answers: see [Proxying](https://vlatte.dev/http/proxying.md). ## Templates A mock file is plain JSON or YAML. Only the settings that are templates, such as a templated body, render: see [Templates](https://vlatte.dev/templating/index.md). ## Errors A file with problems isn't loaded. The error lists every problem with its key and a fix: ``` mock: 2 problems file testdata/vlatte/_mocks/users_mock.yaml: unknown_key: response.stauts is not a known setting; did you mean response.status? file testdata/vlatte/_mocks/users_mock.yaml: conflict: proxy is set with response; keep one of them ``` --- Source: https://vlatte.dev/http/tls.md # TLS `m.MustStartTLS()` serves HTTPS, with no certificate to make: `m.URL()` starts with `https://`. ```go m := mockhttp.MustNew() m.MustStartTLS() t.Cleanup(m.Close) ``` Its default certificate is the test certificate of `net/http/httptest`, which clients skip verifying, or you give the server one of yours with `Setup().TLSCertKeyPair(certFile, keyFile)`. Mutual TLS, the CLI's `--https`, Docker and the admin API are on the [TLS](https://vlatte.dev/tls/index.md) page, for HTTP and gRPC alike. For TLS between the server and the services it proxies to, see [Proxying](https://vlatte.dev/http/proxying.md): `ssl_verify` on proxy replies, and [HTTPS interception](https://vlatte.dev/http/proxying.md#https-interception) for the forward proxy. --- Source: https://vlatte.dev/http/cors.md # CORS Enable CORS when a browser app calls the mock API from another origin. An origin includes its scheme, hostname and port, so `http://localhost:5173` differs from `http://127.0.0.1:5173`. [Testing a frontend client](https://vlatte.dev/guides/testing-a-frontend-client.md) connects a browser app to VLatte from start to end. `Setup().CORS(...)` answers CORS requests, preflights included, with the headers of its configuration. `mockhttp.CORS()` builds one, and `mockhttp.DefaultCORSConfig` is the default: any origin, the methods `GET`, `POST`, `HEAD`, `PUT`, `DELETE` and `PATCH`, the headers a preflight asks for, and `204` for a preflight. **Go** ```go m := mockhttp.MustNew(mockhttp.Setup().CORS(mockhttp.CORS(). AllowOrigin("https://app.example.com"). AllowCredentials(true). ExposeHeaders("X-Request-Id"))) ``` **CLI** ```sh vlatte --addr=127.0.0.1:8080 \ --cors-allowed-origin=http://localhost:5173 --cors-allow-credentials ``` **Configuration** ```yaml # .vlatterc.yaml cors: allowed_origin: http://localhost:5173 allow_credentials: true expose_headers: X-Request-Id ``` Use `allow_credentials` when the browser request includes credentials, such as cookies with `fetch`'s `credentials: 'include'`. Set the web app's origin as `allowed_origin`. CORS on the mock API doesn't configure the separate admin API; use the admin API from your test runner or a terminal as the frontend guide does. | Go | Setting | Sets | |---|---|---| | `AllowOrigin(origins...)` | `cors.allowed_origin` | `Access-Control-Allow-Origin`. | | `AllowMethods(methods...)` | `cors.allowed_methods` | `Access-Control-Allow-Methods`. | | `AllowedHeaders(headers...)` | `cors.allowed_headers` | `Access-Control-Allow-Headers`. | | `ExposeHeaders(headers...)` | `cors.expose_headers` | `Access-Control-Expose-Headers`. | | `AllowCredentials(v)` | `cors.allow_credentials` | `Access-Control-Allow-Credentials`. | | `MaxAge(seconds)` | `cors.max_age` | `Access-Control-Max-Age`. | | `SuccessStatusCode(code)` | `cors.success_status_code` | The status of a preflight. | In settings, `cors: true` (`--cors`) turns CORS on with the defaults, and setting any `cors` key turns it on too. An answer that sends a CORS header, such as a proxied one, answers CORS by itself: the server's CORS headers aren't sent with it. A `Handler` reply writes its answer on top of the server's CORS headers: it should `Set` its CORS headers, or `Del` the server's. Preflights the server answers aren't [journaled](https://vlatte.dev/more/journal.md). --- Source: https://vlatte.dev/http/troubleshooting.md # Troubleshooting Diagnose unmatched requests and serving failures through near misses, debug headers, logs and the request journal. ## Failures When no mock matches, or serving a request fails, the server answers itself. It sends `418 I'm a teapot`, a status a real server rarely sends, so the answer never looks like a mock's, and the `VLatte-Error` header says why: | `VLatte-Error` | Why | |---|---| | `no-match` | No mock matched the request. | | `match` | Finding a mock failed: a matcher, or the store, returned an error. | | `reply` | A mock matched, and its reply failed: a template, a body file, a template data provider or a response mapper. | | `panic` | Serving the request panicked. | The body says the same, and names the mock and the error. A test checks the header: ```go require.Equal(t, vlatte.FailureReply, res.Header.Get(vlatte.HeaderFailure)) ``` The status of `no-match` is `no_match_status_code`, and the status of the others is `error_status_code`. Both default to 418. ## Near misses The answer to a request no mock matched lists the mocks that came closest, as does its [journal](https://vlatte.dev/more/journal.md) entry: the parts of the request each matched, those that differ, and those it didn't check, such as a `times` not used up, which a report never runs. A `times` used up, or a scenario in another state, is a part a mock differs in. The answer is a Markdown document, which reads the same raw, in a terminal or a test's failure, as rendered: ```md # Request did not match No mock matched `GET http://127.0.0.1:8080/users`. Request ID: `0f8c2b1e-6d4a-4c3e-9a57-2b8e1d4f7c90` ## Closest mocks 1. `users` (`list users`): 2 of 3 parts matched; differs in `header Accept` 2. `create`: 1 of 2 parts matched; differs in `method` ## Next steps - Change the request, or the closest mock, in the parts they differ in. - See the request in the journal: `GET http://127.0.0.1:8081/requests/0f8c2b1e-6d4a-4c3e-9a57-2b8e1d4f7c90` ``` The journal line shows when the server keeps a journal and serves the [admin API](https://vlatte.dev/admin-api/index.md), as the `vlatte` command does. Every name and value is in a code span, so none can change the structure of the document. The report runs no matcher twice: it goes on from the part where matching stopped, which a mock differs in, so it runs only the matchers matching never reached, the action of a `match.Peek` among them. It runs none that keeps state, such as `times`, which it lists as not checked; a scenario's matcher keeps none as it matches, so it runs. A mock that wants the request but is used up, by its `Times`, differs in `times` only, and comes first. `near_misses: false`, `--near-misses=false` or `mockhttp.Setup().NoNearMisses()` turns it off. ### Formats The application under test often asks for JSON, and fails to decode anything else. The answer follows the request's `Accept` header, and adds `Accept` to `Vary`: | `Accept` prefers | Answer | `Content-Type` | |---|---|---| | `application/json`, or a JSON type such as `application/vnd.api+json` | Problem details | `application/json` | | `application/problem+json`, over `application/json` | Problem details | `application/problem+json` | | `text/markdown` | The Markdown document | `text/markdown; charset=utf-8` | | Anything else, `*/*`, or no `Accept` | The Markdown document | `text/plain; charset=utf-8` | Of the types `Accept` weighs the same, the one a more specific range names wins, then the one it lists first: `application/json, text/plain, */*` gets JSON, `*/*, application/json` gets JSON too, and a request that accepts only `*/*` gets text. `application/problem+json` wins only when `Accept` prefers it to `application/json`, by weight or by a more specific range. Problem details are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) members, with the request and the near misses as the journal gives them: ```json { "type": "https://caffeine-projects.github.io/vlatte/http/troubleshooting#near-misses", "title": "Request did not match", "status": 418, "detail": "No mock matched GET http://127.0.0.1:8080/users.", "request_id": "0f8c2b1e-6d4a-4c3e-9a57-2b8e1d4f7c90", "request": {"method": "GET", "url": "http://127.0.0.1:8080/users"}, "near_misses": [ {"mock_id": "users", "name": "list users", "passed": 2, "total": 3, "failed": ["header Accept"]} ] } ``` `no_match_format`, `--no-match-format` or `mockhttp.Setup().NoMatchFormat(...)` sends one format whatever `Accept` asks: `auto`, the default, `text`, `markdown`, `json` or `problem`. ### Custom answers A server made in Go can answer in a shape of its own, such as the error body of the API a mock stands in for. A `NoMatchRenderer` gets the request, the near misses and the status, and writes the answer; `VLatte-Error` is set before it runs, and the journal records the request whatever it sends. It can hand requests to the default renderer: ```go type apiNoMatch struct{ fallback mockhttp.NoMatchRenderer } func (a apiNoMatch) RenderNoMatch(w http.ResponseWriter, nm *mockhttp.NoMatch) { if !strings.HasPrefix(nm.Request.URL.Path, "/api/") { a.fallback.RenderNoMatch(w, nm) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, `{"code":"NOT_FOUND","closest":%d}`, len(nm.NearMisses)) } m := mockhttp.MustNew(mockhttp.Setup(). NoMatchRenderer(apiNoMatch{fallback: mockhttp.DefaultNoMatchRenderer(mockhttp.NoMatchAuto)})) ``` The server calls a renderer for many requests at once. One that writes nothing answers with the no-match status and no body. A renderer and a format other than `auto` don't go together: the format configures the default renderer, and building the server fails. ## Debug headers Each answer says how the server served it, in `VLatte-*` headers: | Header | Value | |---|---| | `VLatte-Matched-Mock-Id` | The ID of the mock that matched. | | `VLatte-Matched-Mock-Name` | Its name, when it has one. | | `VLatte-Matched-Mock-Hits` | Its hits, this request included. | | `VLatte-Matched-Mock-Source` | The file it was loaded from, when it was loaded from one. | | `VLatte-Request-Id` | The ID of the request: `.Request.ID` in templates, `request_id` in the logs, its id in the [journal](https://vlatte.dev/more/journal.md). Every answer has it, failures and no-match included. | | `VLatte-Proxied` | `true` when a real service answered instead of a mock: a mock that replies with `From(...)`, a proxy passing on a request no mock matches, or the target of a recording. | A failure keeps the headers of the mock that matched before it failed. Hits count once the answer is sent, so two requests served at the same time can show the same number. A test checks them like any header: ```go require.Equal(t, "users-get", res.Header.Get(vlatte.HeaderMatchedMockID)) ``` Header names don't depend on case: Go sends `Vlatte-Matched-Mock-Id`, and HTTP/2 sends them in lower case. Recordings leave the debug headers out. `debug_headers: false` turns them all off, as do `VLATTE_DEBUG_HEADERS=false`, `--debug-headers=false` and `mockhttp.Setup().NoDebugHeaders()`. `VLatte-Error` stays: it is not a debug header. ## Logs The server logs each request it serves, and how. `Setup().LogLevel(...)` and `Setup().LogVerbosity(...)` set what it logs, `Setup().UseDescriptiveLogger()` (`--debug` for the command) prints a readable block for each request in place of structured lines, `Setup().RedactHeader(...)` hides the values of headers, and `Setup().Silent()` logs nothing. The [admin API](https://vlatte.dev/admin-api/index.md) streams the log lines at `GET /logs`, and what the server serves at `GET /events`. --- Source: https://vlatte.dev/grpc/index.md # gRPC `mockgrpc` runs gRPC mock servers for tests, and for the `vlatte` command. A gRPC mock server serves any method of the services whose generated code the program imports: it needs no implementation registered. A mock names its method, matches the calls, and answers them, whatever their type: unary, server streaming, client streaming or bidi streaming. ```go func TestGreeter(t *testing.T) { m := mockgrpc.MustNew(mockgrpc.Setup().InMemory()) // no port: only m.Dial reaches it m.MustStart() t.Cleanup(m.Close) m.MustMock(mockgrpc.Method("greeter.Greeter/SayHello"). Header("x-team", match.Equal("core")). Field("name", match.Equal("ann")). Reply(mockgrpc.Message(&pb.HelloReply{Message: "hi ann"}))) client := pb.NewGreeterClient(m.MustDial()) // closed with the server res, err := client.SayHello(metadata.AppendToOutgoingContext(t.Context(), "x-team", "core"), &pb.HelloRequest{Name: "ann"}) // ... } ``` `Setup().InMemory()` serves on an in-memory listener, with no port: only `m.Dial()` reaches it. Without it, the server listens on a local port, which `m.Addr()` names, for clients of any language. ## Mock features As HTTP mocks, gRPC mocks have an `ID`, a `Name` and a `Priority`, can start `Enabled(false)`, serve a number of calls with `Times` and `Once`, take part in [scenarios](https://vlatte.dev/scenarios/index.md) with `StartScenario`, `ScenarioIs`, `ScenarioStateIs`, `ScenarioStateWillBe` and `ScenarioKey`, wait before replying with `Delay` and `DelayFunc`, and run [on_complete events](https://vlatte.dev/webhooks/index.md), such as webhooks, with `OnCompleteEvent`, and callbacks with `Callback`. An answer with a status counts as the mock's answer, for `Times` too. A scenario moves when its mock is picked to answer, before it answers. See [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md), [Scenarios](https://vlatte.dev/scenarios/index.md) and [Webhooks and events](https://vlatte.dev/webhooks/index.md). ## The server A gRPC server keeps a [journal](https://vlatte.dev/more/journal.md#in-grpc) of its calls with `Setup().Journal(dir)`, which tests [verify](https://vlatte.dev/verify/index.md) as HTTP tests do, serves an [admin API](https://vlatte.dev/admin-api/index.md) with `Setup().Admin(admin.MustNew())`, whose mocks `Setup().AdminStore(mockgrpc.FileStore(dir))` [keeps](https://vlatte.dev/admin-api/index.md#persistent-admin-mocks) across restarts, and answers a call no mock matched with `Setup().NoMatchCode(code)`, `NotFound` by default. `Setup().ServerOptions(...)` passes options to the `grpc.Server`, such as interceptors. ## In this section - [Methods](https://vlatte.dev/grpc/methods.md): naming the method a mock answers. - [Request matching](https://vlatte.dev/grpc/matching.md): metadata, fields and messages. - [Replies](https://vlatte.dev/grpc/replies.md): messages, statuses, sequences and functions. - [Streaming](https://vlatte.dev/grpc/streaming.md): server, client and bidi streams. - [Cuts](https://vlatte.dev/grpc/cuts.md): a server that dies midway. - [Descriptors and reflection](https://vlatte.dev/grpc/descriptors.md): serving services without their generated code. - [Proxying](https://vlatte.dev/grpc/proxying.md): answers of another gRPC server. [Recording](https://vlatte.dev/recording/grpc.md) has a section of its own. - [Mock files](https://vlatte.dev/grpc/mock-files.md): the YAML and JSON format. - [TLS](https://vlatte.dev/grpc/tls.md). - [Troubleshooting](https://vlatte.dev/grpc/troubleshooting.md): no-match codes, trailers and debug metadata. What gRPC shares with HTTP, such as [matchers](https://vlatte.dev/matchers/index.md), [scenarios](https://vlatte.dev/scenarios/index.md), [templates](https://vlatte.dev/templating/index.md#in-grpc), [webhooks](https://vlatte.dev/webhooks/index.md#in-grpc) and the [journal](https://vlatte.dev/more/journal.md#in-grpc), works the same for both: each has a section of its own in the sidebar. --- Source: https://vlatte.dev/grpc/methods.md # Methods `Method` takes a method by whole segments of its full method: `SayHello`, `Greeter/SayHello`, `greeter.Greeter/SayHello`, or the full method, such as `pb.Greeter_SayHello_FullMethodName`. A mock fails to build, rather than wait for calls that never come, when its name names no method (`mockgrpc.ErrUnknownMethod`: a typo), or methods of different call types (`mockgrpc.ErrAmbiguousMethod`: name the service). A call of a method no descriptor describes answers `Unimplemented`. ```go m.MustMock(mockgrpc.Method("SayHello").Reply(mockgrpc.Message(&pb.HelloReply{Message: "hi"}))) m.MustMock(mockgrpc.Method(pb.Greeter_SayHello_FullMethodName).Reply(mockgrpc.Message(&pb.HelloReply{Message: "hi"}))) ``` ## Call types A mock answers its method whatever its call type: unary, server streaming, client streaming or bidi streaming. A reply is checked against it when the mock is built: see [Replies](https://vlatte.dev/grpc/replies.md) and [Streaming](https://vlatte.dev/grpc/streaming.md). --- Source: https://vlatte.dev/grpc/matching.md # Request matching They read the call's metadata and its request, as protojson writes it, with the field names of the `.proto` file: enums by name, `int64` values as text, timestamps in RFC 3339. | Matcher | Matches | |---|---| | `Header(key, m)` | A metadata key. | | `Field(path, m)` | The request at a JSON path, such as `user_id` or `items[0].key`. A client stream's request is the array of its messages: `$[0].key`. | | `Message(msg)`, `Messages(msgs...)` | A request equal to `msg` by `proto.Equal`; a client stream's messages, in order. | | `JSON(text)` | A request equal to the message `text` holds, read as protojson reads it, so `{"count": 42}` and `{"count": "42"}` match the same call. | | `Body(m)` | The request as JSON text, such as `match.Contain("ann")`. | | `CallMatches(fn)` | A call `fn` reports true for. | | `Matches(m)` | A call `m` matches: it gets the `*mockgrpc.Call`. | A client streaming call matches once the client closed its side, on all its messages. A bidi call matches as it starts, on its method and metadata: its `Bidi` reply matches each message. ```go m.MustMock(mockgrpc.Method("users.Users/Get"). Header("authorization", match.HasPrefix("Bearer ")). Field("id", match.RegExpr("^u-")). Reply(mockgrpc.JSON(`{"id": "u-1", "name": "ann"}`))) ``` The matchers they take, such as `match.Equal`, `match.RegExpr` or `match.JSONPath`, are shared by every protocol: see [Matchers](https://vlatte.dev/matchers/index.md). In a [mock file](https://vlatte.dev/grpc/mock-files.md), `request.header`, `request.fields`, `request.message`, `request.messages`, `request.body` and `request.custom` match the same parts. --- Source: https://vlatte.dev/grpc/replies.md # Replies Build gRPC messages and status replies, sequence or randomize answers, and generate responses from the incoming call. | Reply | Answers | |---|---| | `Message(msg)` | A message, which must be of the method's output type. | | `Messages(msgs...)` | The messages of a server or bidi stream, in turn. | | `JSON(texts...)` | The messages `texts` hold, in the method's output type. | | `Template(texts...)` | Like `JSON`, rendered for each call: `{"message": "hi {{ .Request.Message.name }}"}`, with `.Request.Header`, `.KV`, `.Scenarios.State "name"`, `.Scenarios.StateOf "name" "key"`, `.Data`, which `m.SetData` sets, and `.Provided.`, which `Setup().TemplateDataProviders(...)` provide. | | `Status(code, msg)` | A status, with no message. | | `Bidi()` | A bidi stream, message by message: `On(matcher, reply)`, `Otherwise(reply)`, `OnEnd(reply)`. | | `Seq(replies...)` | Each reply in turn, one per call, a stream as a whole, then `OnSequenceEnded(reply)`'s. A call past the end without it fails with the `vlatte-error` trailer. | | `Rand(replies...)` | One of the replies at random for each call. `RandWithSeed(seed, replies...)` picks in an order the seed repeats, and `RandWith(r, replies...)` with a `*rand.Rand` of yours. | | `Echo()` | The client's messages back, read as the method's output type by field number: a unary or server streaming call's request, each message of a bidi call as it comes, the last one of a client stream. `Log()` logs each. | | `Shared(replier)` | The same `Replier` for every mock built with it, which they never close. | | `Func(fn)` | Whatever `fn` sends through the call, for anything else. | | `From(target)` | The answer of another gRPC server: see **Proxy replies** below. | A reply's `Header` and `Trailer` add metadata, `Status` ends a stream with a status after its messages, `Details` adds `errdetails` messages to it, and `MessageDelay` waits before each message. `Build` checks a reply against the method: its messages' type, and that a unary or client streaming method gets one message, or a status. It checks each reply of `Seq` and `Rand` the same way, and each mock built with them goes through its replies on its own. ```go m.MustMock(mockgrpc.Method("Users/Get"). Reply(mockgrpc.Status(codes.InvalidArgument, "bad id"). Details(&errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{Field: "id"}}}))) m.MustMock(mockgrpc.Method("Feed/Watch"). Reply(mockgrpc.Messages(&pb.Event{Id: "1"}, &pb.Event{Id: "2"}). MessageDelay(100 * time.Millisecond). Status(codes.Unavailable, "restarting"))) m.MustMock(mockgrpc.Method("Chat/Talk").Reply(mockgrpc.Bidi(). On(match.Field("text", match.Equal("hi")), mockgrpc.Message(&pb.Line{Text: "hello"})). Otherwise(mockgrpc.JSON(`{"text": "?"}`)). OnEnd(mockgrpc.Message(&pb.Line{Text: "bye"})))) ``` ## Sequences and random replies `Seq(replies...)` answers each call with the next reply, then the reply of `OnSequenceEnded`; `Rand(replies...)` with one at random. In a [mock file](https://vlatte.dev/grpc/mock-files.md), `response_sequence` and `response_random`, as for [HTTP](https://vlatte.dev/http/replies.md#sequences). ## Proxy replies `From(target)` answers with what another gRPC server answers: see [Proxying](https://vlatte.dev/grpc/proxying.md). --- Source: https://vlatte.dev/grpc/streaming.md # Streaming A mock answers its method whatever its call type. `Build` checks a reply against it: a unary or client streaming method gets one message, or a status; a server or bidi streaming method gets any number of messages. ## Server streaming `Messages(msgs...)` sends the messages of the stream in turn. `MessageDelay(d)` waits before each message, and `Status(code, msg)` ends the stream with a status after its messages. ```go m.MustMock(mockgrpc.Method("Feed/Watch"). Reply(mockgrpc.Messages(&pb.Event{Id: "1"}, &pb.Event{Id: "2"}). MessageDelay(100 * time.Millisecond). Status(codes.Unavailable, "restarting"))) ``` The client reads the two events, then `Unavailable`. ## Client streaming A client streaming call matches once the client closed its side, on all its messages. Its request is the array of its messages: `Field("$[0].key", m)` reads the first, and `Messages(msgs...)` matches them all, in order. ```go m.MustMock(mockgrpc.Method("Upload/Send"). Field("$[0].name", match.Equal("first")). Reply(mockgrpc.JSON(`{"count": 2}`))) ``` `Echo()` answers a client stream with its last message, read as the method's output type by field number. ## Bidi streaming A bidi call matches as it starts, on its method and metadata. Its `Bidi()` reply then matches each message the client sends: | Method | Answers | |---|---| | `On(matcher, reply)` | A message the matcher matches, with the reply's messages. Rules are tried in order. | | `Otherwise(reply)` | A message no rule matches. | | `OnEnd(reply)` | The end of the client's side, with the reply's messages, before the stream ends. | ```go m.MustMock(mockgrpc.Method("Chat/Talk").Reply(mockgrpc.Bidi(). On(match.Field("text", match.Equal("hi")), mockgrpc.Message(&pb.Line{Text: "hello"})). Otherwise(mockgrpc.JSON(`{"text": "?"}`)). OnEnd(mockgrpc.Message(&pb.Line{Text: "bye"})))) ``` The client that sends `hi`, then `what`, then closes its side reads `hello`, `?` and `bye`. `Echo()` answers each message of a bidi call as it comes. In a [mock file](https://vlatte.dev/grpc/mock-files.md), `bidi` takes `header`, `trailer`, `on` (rules of `when`, a matcher list of the message, and `response`), `otherwise` and `on_end`. ```yaml request: method: chat.Chat/Talk bidi: on: - when: [field, text, [eq, hi]] response: message: text: hello otherwise: message: text: "?" on_end: message: text: bye ``` ## Cutting a stream A `Func` reply that returns `vlatte.ErrAbort` or `vlatte.ErrReset` cuts the call after the messages it sent: see [Cuts](https://vlatte.dev/grpc/cuts.md). --- Source: https://vlatte.dev/grpc/cuts.md # Cuts A reply that returns an error matching `vlatte.ErrAbort` or `vlatte.ErrReset`, such as a `Func` reply, cuts the call, to test a client against a server that dies midway. The mock counts the call and runs its events, then the server cuts the call's connection after the messages of a stream the reply sent, before any status: the client reads `Unavailable`, and no trailer. `vlatte.ErrReset` resets a TCP connection, so the client reads `ECONNRESET`, and `vlatte.ErrAbort` ends it. grpc can't reset one stream from a handler, so the other calls on the connection end too. A server whose `ServerOptions` set `grpc.Creds` can't cut its connections: the call fails, with the `vlatte-error` trailer. ```go m.MustMock(mockgrpc.Method("Feed/Watch").Reply(mockgrpc.Func(func(_ context.Context, call *mockgrpc.Call) error { if err := call.Send(&pb.Event{Id: "1"}); err != nil { return err } return vlatte.ErrReset // after event 1, the client reads a reset }))) ``` --- Source: https://vlatte.dev/grpc/descriptors.md # Descriptors and reflection A server without the generated code of a service serves it from its descriptors, with messages of dynamic types: `Setup().Descriptors("api.binpb")` loads descriptor sets, as `protoc --descriptor_set_out=api.binpb --include_imports` or `buf build -o api.binpb` write them, and `Setup().Files(files)` takes a `*protoregistry.Files`. ```sh protoc --descriptor_set_out=api.binpb --include_imports api.proto ``` ```go m := mockgrpc.MustNew(mockgrpc.Setup().Descriptors("api.binpb").Reflection()) ``` ## Reflection `Setup().Reflection()` serves server reflection, so `grpcurl` lists the services and describes their messages. The `vlatte` command serves it by default: `--grpc-reflection=false` turns it off. ```sh grpcurl -plaintext localhost:50051 list grpcurl -plaintext -d '{"id": "u-1"}' localhost:50051 users.Users/Get ``` --- Source: https://vlatte.dev/grpc/proxying.md # Proxying Forward gRPC calls to an upstream service, including streaming calls, with configured targets and metadata. ## Proxy replies `From(target)` answers each call with the answer of another gRPC server, of any call type: the target gets the client's metadata and messages, and the client gets the target's header, messages, trailer and status as they come, a bidi call message by message. `Header` sets metadata the target gets in place of the client's, `RemoveHeaders` leaves some out, and `DialOptions` sets options of the connection, which goes without TLS by default, such as `grpc.WithTransportCredentials`. A target's status is the mock's answer; a target it can't reach fails the call, with the `vlatte-error` trailer. ```go m.MustMock(mockgrpc.Method("users.Users/Get").Reply(mockgrpc.From("localhost:50051").Header("authorization", "Bearer test"))) ``` ## Outbound rules `grpc.outbound.allow` and `grpc.outbound.deny` limit the addresses proxy replies and the [recording](https://vlatte.dev/recording/grpc.md) target connect to: see [Outbound rules](https://vlatte.dev/http/proxying.md#outbound-rules). --- Source: https://vlatte.dev/grpc/mock-files.md # Mock files `Setup().MockFiles("testdata/grpc/*.yaml")` loads gRPC mock files at `Start`, and `m.Reload()` again; `mockgrpc.FromFile` and `FromBytes` build one. Their keys mirror the Go API, and [`mockgrpc/schema/grpc.schema.json`](https://vlatte.dev/reference/schemas.md) describes them for editors: ```yaml id: denied priority: 1 request: method: users.Users/Get header: authorization: [hasprefix, "Bearer "] fields: id: [regex, "^u-"] response: status: code: PERMISSION_DENIED message: not yours details: - "@type": type.googleapis.com/google.rpc.ErrorInfo reason: NOT_OWNER ``` Start a YAML file with `# yaml-language-server: $schema=https://vlatte.dev/schemas/grpc.schema.json` for completion and checks in editors, or give a JSON file a `"$schema"` key: see [Mock file schemas](https://vlatte.dev/reference/schemas.md). ## Keys `request.message` and `request.messages` match whole messages, `request.body` takes a matcher of the request's JSON, and `request.custom` lists matchers of the whole call, as `Matches` does. `response` takes `message`, `messages`, `body_file` (a JSON file of a message or a list of them), `template`, `header`, `trailer`, `status` and `message_delay`; `bidi` takes `header`, `trailer`, `on` (rules of `when` and `response`), `otherwise` and `on_end`; `proxy` takes `target`, `header` and `remove_headers`. `response_sequence` takes `responses`, a list of `response`s, and `after_ended`, a `response`; `response_random` takes `responses` and `seed`. `delay`, `repeat`, `scenario` and `on_complete` work as in HTTP mock files. `Setup().Matchers(registry)` gives a server the custom matchers its mock files name, as it does an HTTP one: see [Mock files](https://vlatte.dev/http/mock-files.md). --- Source: https://vlatte.dev/grpc/tls.md # TLS `m.MustStartTLS()` serves gRPC over TLS, with a self-signed certificate for `localhost`, `127.0.0.1` and `::1`, and `m.Dial()` already trusts it: ```go m := mockgrpc.MustNew() m.MustStartTLS() t.Cleanup(m.Close) client := pb.NewUsersClient(m.MustDial()) ``` `m.Certificate()` returns the certificate for clients of your own to trust. Credentials `ServerOptions` set with `grpc.Creds` win over these: `StartTLS` then serves them as `Start` does, `m.Certificate()` returns `nil`, and `m.Dial()` needs `grpc.WithTransportCredentials`. Certificates of your own, mutual TLS and the CLI's `--grpc-tls` are on the [TLS](https://vlatte.dev/tls/index.md) page, for HTTP and gRPC alike. --- Source: https://vlatte.dev/grpc/troubleshooting.md # Troubleshooting Use status codes, diagnostic metadata and the request journal to find why a call did not get its expected reply. ## Failures A gRPC status says how the call ended. The `vlatte-error` trailer identifies an answer from vlatte itself and says what failed: | What happened | Status | `vlatte-error` | |---|---|---| | No mock matched. | `NotFound`, unless `Setup().NoMatchCode` sets another. | `no-match` | | No descriptor describes the method. | `Unimplemented` | `no-match` | | A matcher or the mock store failed. | `Internal` | `match` | | A reply failed, such as a template or a call past the end of a `Seq`. | `Internal` | `reply` | | Serving the call panicked. | `Internal` | `panic` | A mock that answers with a status, as `Status(...)` does, gets no `vlatte-error` trailer: that status is its answer. Check the trailer to distinguish an intentional mock error from a failure to serve the mock. For a unary call that no mock matches, with the default no-match code: ```go var trailer metadata.MD _, err := client.SayHello(ctx, &pb.HelloRequest{Name: "ann"}, grpc.Trailer(&trailer)) fmt.Println(status.Code(err)) // NotFound fmt.Println(trailer.Get("vlatte-error")) // [no-match] ``` `metadata` and `status` are `google.golang.org/grpc/metadata` and `google.golang.org/grpc/status`. For a stream, read `stream.Trailer()` after `Recv` returns an error, including `io.EOF`, or after `CloseAndRecv` returns. ## Debug metadata A gRPC server sends `vlatte-matched-mock-id`, `vlatte-matched-mock-name` and `vlatte-matched-mock-hits` as header metadata, with a mock's message or status, and `vlatte-request-id` when it keeps a journal. `Name(...)` on a mock builder sets the name. `mockgrpc.Setup().NoDebugHeaders()` turns them off. Keep a [journal](https://vlatte.dev/more/journal.md#in-grpc) to inspect the request's metadata, message and outcome, and use its request ID to find the call through the [admin API](https://vlatte.dev/admin-api/index.md). [Request verification](https://vlatte.dev/verify/index.md) reports the closest requests when an expected call didn't arrive. ## Shutdown `Close` waits for the calls in flight up to `Setup().ShutdownTimeout(d)`, 5s by default, then cuts them. --- Source: https://vlatte.dev/more/index.md # Features Use these features to describe realistic API behavior, reuse mocks across tests and tools, and check what an application sent. HTTP and gRPC share matchers, scenarios, recording and request verification; each page says where the protocols differ. | Feature | What it helps you test | |---|---| | [SSE (Server-sent Events)](https://vlatte.dev/sse/index.md) | Streaming HTTP clients, event timing, reconnects and malformed event data. | | [Matchers](https://vlatte.dev/matchers/index.md) | Request methods, paths, headers and structured values such as JSON, XML and JWT claims. | | [Templating](https://vlatte.dev/templating/index.md) | Replies that use values from the request, files or a key-value store. | | [Scenarios](https://vlatte.dev/scenarios/index.md) | Stateful flows, including separate state for each caller or test key. | | [Recording](https://vlatte.dev/recording/index.md) | Capture a real HTTP or gRPC service as mock files and replay it without the service. | | [Verifying requests](https://vlatte.dev/verify/index.md) | Check which requests arrived, how many, by when and in what order. | | [Webhooks and events](https://vlatte.dev/webhooks/index.md) | Trigger a callback after a mock responds. | | [TLS](https://vlatte.dev/tls/index.md) | HTTPS, gRPC over TLS and mutual TLS. | | [Recipes](https://vlatte.dev/recipes/index.md) | Reuse prepared mocks, such as OAuth and OpenID Connect providers. | | [Mock lifecycle](https://vlatte.dev/more/mock-lifecycle.md) | IDs, names, priorities, `Times`, delays, enabling and disabling. | | [Request journal](https://vlatte.dev/more/journal.md) | The requests a server got, to verify after the fact. | Start with [HTTP](https://vlatte.dev/http/index.md) or [gRPC](https://vlatte.dev/grpc/index.md) for protocol-specific matching and replies. Use the [admin API](https://vlatte.dev/admin-api/index.md) to manage a running server, or connect a [coding agent](https://vlatte.dev/agents/index.md) over MCP. The [guides](https://vlatte.dev/guides/index.md) combine the features into complete testing tasks. --- Source: https://vlatte.dev/sse/index.md # SSE (Server-sent Events) An HTTP mock replies with [server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html): a `text/event-stream` body, sent event by event, each flushed to the client as it goes. Use it to test clients of streamed LLM answers, live feeds, job progress, notifications and MCP servers over Streamable HTTP. **Go** ```go m.MustMock(mockhttp.Getf("/prices").Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Name("price").ID("1").DataJSON(map[string]any{"value": 10}), mockhttp.NewEvent().Name("price").ID("2").DataJSON(map[string]any{"value": 11}).Delay(time.Second), )))) ``` **Mock file** ```yaml request: method: GET path: /prices response: status: 200 event_stream: events: - event: price id: "1" data: value: 10 - event: price id: "2" data: value: 11 delay: 1s ``` **CLI** ```sh vlatte --addr=127.0.0.1:8080 # with the mock file under testdata/vlatte/_mocks curl -N http://127.0.0.1:8080/prices ``` The client gets the first event at once, and the second a second later: ```text event: price id: 1 data: {"value":10} event: price id: 2 data: {"value":11} ``` ## What the reply sends - **Headers.** `Content-Type: text/event-stream` and `Cache-Control: no-cache`, unless the reply sets them: a `Content-Type` header can add a charset, or name another type to test a client that refuses it. The headers go out before the first event, so a client opens the stream even when nothing is due yet. - **Flushing.** Each event is flushed as it goes, over HTTP/1.1 and HTTP/2, and gzipped too, with `Gzip()` or `encoding: gzip`. - **HEAD.** A `HEAD` request gets the headers only. - **Status.** A stream needs a status with a body: a 1xx, `204` or `304` fails the mock. - **Ending.** A stream ends after its last event, unless it [keeps open](https://vlatte.dev/sse/timing.md#keeping-open). The mock counts the request [once the stream starts](https://vlatte.dev/sse/timing.md#how-a-stream-counts). A mock with an event stream is a mock as any other: it matches requests, takes part in [scenarios](https://vlatte.dev/scenarios/index.md), runs [callbacks and events](https://vlatte.dev/webhooks/index.md), and can be added over the [admin API](https://vlatte.dev/admin-api/index.md) and [MCP](https://vlatte.dev/mcp/index.md). Only HTTP mocks reply with events: gRPC has [streams](https://vlatte.dev/grpc/streaming.md) of its own. > **Tip** > > The admin API's `GET /events` and `GET /logs` are server-sent events too, but of vlatte itself: what the server serves and logs. See [Admin API](https://vlatte.dev/admin-api/index.md#routes). ## Pages | Page | What it covers | |---|---| | [Events](https://vlatte.dev/sse/events.md) | The fields of an event, text and JSON data, and templated events. | | [Timing and lifetime](https://vlatte.dev/sse/timing.md) | Delays, heartbeats, streams that keep open, and how a stream counts. | | [Reconnecting](https://vlatte.dev/sse/reconnecting.md) | `retry`, `Last-Event-ID`, resuming, and telling clients to stop. | | [Testing a client's parser](https://vlatte.dev/sse/raw-events.md) | Raw events, cut connections, and other faults. | | [With other features](https://vlatte.dev/sse/other-features.md) | Proxying, recording, CORS, the admin API and per-user feeds. | Two guides put the parts together: [Testing an LLM streaming client](https://vlatte.dev/guides/llm-streaming.md) and [Mocking a remote MCP server](https://vlatte.dev/guides/mcp-server.md). --- Source: https://vlatte.dev/sse/events.md # Events `NewEventStream(events...)` in Go, and `event_stream.events` in a mock file, list the events of a stream, sent in order. Each event's fields go on the wire as the [event stream format](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation) reads them. **Go** ```go mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Name("price").ID("1").DataJSON(map[string]any{"value": 10}), mockhttp.NewEvent().Comment("keep-alive"), mockhttp.NewEvent().Data("line 1\nline 2"), mockhttp.NewEvent().DataJSON("done"), ).Retry(3 * time.Second)) ``` **Mock file** ```yaml response: status: 200 event_stream: retry: 3s events: - event: price id: "1" data: value: 10 # text as is, anything else as JSON - comment: keep-alive - data: "line 1\nline 2" # a data line per line - data_json: "done" # always JSON: sends "done", quotes too ``` ```text retry: 3000 event: price id: 1 data: {"value":10} : keep-alive data: line 1 data: line 2 data: "done" ``` ## Fields | Go | Mock file | Sends | |---|---|---| | `Name(name)` | `event` | The event type. Clients dispatch an event without one as a `message`. | | `ID(id)` | `id` | The id, which clients send back as `Last-Event-ID` when they [reconnect](https://vlatte.dev/sse/reconnecting.md). An empty id is sent too: it resets the client's last event ID. | | `Data(text)` | `data` | The data. In a mock file, text is sent as it is, and any other value as JSON but `null`, which fails: write `""` for empty data, or `data_json: null`. | | `DataJSON(v)` | `data_json` | The data as JSON, strings and `null` too. A value that doesn't marshal fails the mock. | | `Retry(d)` | `retry` | How long the client waits before it reconnects, from this event on, in whole milliseconds. | | `Comment(text)` | `comment` | A comment, sent before the fields, which clients skip. An event with only a comment is a comment. | | `Delay(d)` | `delay` | The wait before the event: see [Timing and lifetime](https://vlatte.dev/sse/timing.md). | | `Raw(text)` | `raw` | Text sent as it is, in place of every field but the delay: see [Testing a client's parser](https://vlatte.dev/sse/raw-events.md). | `EventStream.Retry(d)`, `retry` beside `events`, sends a `retry` field first, before the events. Durations in a mock file are milliseconds or a Go duration, such as `1.5s`. An event takes one of `data` and `data_json`. In Go, each data setter replaces what an earlier one set. ## Line breaks Data and comments with line breaks get a line each, `data: ` or `: ` and the line, so clients read them back whole: `line 1\nline 2` above. CRLF and CR count as line breaks too. An event type or id can't hold a line break, which would end the field there, and an id can't hold a NUL, which clients ignore: the mock fails to build. A stream whose fields are templates fails at the reply that renders one. ## Events without data Clients dispatch an event only when it has data: one with only an id sets the client's last event ID without dispatching anything, and one with only a comment is skipped. Send an empty `data: ""` to dispatch an event with empty data. ## Templated events With `template.enabled` in a mock file, or `Template()` on the stream in Go, the text of the events renders with each request, as [templates](https://vlatte.dev/templating/index.md) do: the event types, ids, comments and text data. **Go** ```go mockhttp.Request().Method(http.MethodGet).URLPathTemplate("/users/{id}/notifications"). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Name("notification").ID("{{ .Request.PathParams.id }}-1"). Data(`{"user": "{{ .Request.PathParams.id }}", "text": "Your order shipped"}`), ).Template())) ``` **Mock file** ```yaml request: method: GET path_template: "/users/{id}/notifications" response: status: 200 event_stream: events: - event: notification id: "{{ .Request.PathParams.id }}-1" data: '{"user": "{{ .Request.PathParams.id }}", "text": "Your order shipped"}' template: enabled: true ``` JSON data, `data_json` (`DataJSON` in Go) and `raw` are sent as they are, never as templates: in a templated mock file, write data that holds a template as text, quoted as above. A template that fails to render fails the reply. A templated stream's description, such as what the [admin API's](https://vlatte.dev/admin-api/index.md#routes) `GET /mocks/{id}` returns, keeps JSON data as JSON: built back, it is sent compact, with its object keys sorted. JSON with a number past int64, which a file can't hold, is written as text, which renders as a template. --- Source: https://vlatte.dev/sse/timing.md # Timing and lifetime A stream sends its events at once, unless they have delays, and ends after the last one, unless it keeps open. **Go** ```go mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Name("price").Data("10"), mockhttp.NewEvent().Name("price").Data("11").Delay(30*time.Second), ).EventDelay(100*time.Millisecond).Heartbeat(15*time.Second).KeepOpen()) ``` **Mock file** ```yaml response: status: 200 event_stream: event_delay: 100ms # before each event without a delay of its own heartbeat: 15s # a comment each time the stream waits this long keep_open: true # stay open after the last event, until the client leaves events: - event: price data: "10" - event: price data: "11" delay: 30s ``` ## Delays | Go | Mock file | Waits | |---|---|---| | `EventDelay(d)` on the stream | `event_delay` | `d` before each event without a delay of its own: a stream that trickles in. A negative `d` waits for none. | | `EventDelayFunc(delay)` on the stream | | What `delay` picks, before each event without a delay of its own, such as `vlatte.Jitter(100*time.Millisecond, 20)`. | | `Delay(d)` on an event | `delay` | `d` before that event, in place of the stream's. | Everything before a wait is flushed first, so the client reads the events before it while it waits. A mock file can't write a function: a stream with `EventDelayFunc` is described without it, such as by `GET /mocks/{id}` on the [admin API](https://vlatte.dev/admin-api/index.md#routes). These delays are between events. The mock's own [delay](https://vlatte.dev/more/mock-lifecycle.md#delays) waits before the reply starts, and [pipes](https://vlatte.dev/http/pipes.md) can slow the bytes of the stream. ## Heartbeats `Heartbeat(d)`, `heartbeat` in a mock file, sends a comment, `: heartbeat`, each time the stream waits `d` without sending anything: while it waits for a delayed event, and while it keeps open after the last one. Clients skip comments, and proxies and load balancers that drop a silent connection keep it. ## Keeping open `KeepOpen()`, `keep_open: true`, keeps the stream open after its last event, as a live feed is, until the client leaves or the server closes. A stream without it ends after its last event, and a client such as `EventSource` [reconnects](https://vlatte.dev/sse/reconnecting.md) after its `retry` time. A stream that keeps open with no events at all sends its headers at once, so the client opens it, then only its heartbeats. ## How a stream counts A stream may stay open as long as its client does, so the mock counts the request once the stream starts, not once it ends: - `Once()` and `Times(n)` serve one client, or `n`, while their streams are still open. The next request goes to another mock. - A [scenario](https://vlatte.dev/scenarios/index.md) moves on once the stream starts, so the next request matches in the new state while the stream is open. - `Hits()` and `AssertCalled` count open streams. The mock runs its [callbacks and `on_complete` events](https://vlatte.dev/webhooks/index.md) once the stream ends, with the body it sent, when that is 1 MiB or less. ```go feed := m.MustMock(mockhttp.Getf("/feed").Once(). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream(mockhttp.NewEvent().Data("hi")).KeepOpen()))) m.MustMock(mockhttp.Getf("/feed").Priority(1).Reply(mockhttp.Status(http.StatusConflict))) // The first client gets the feed, and keeps it open. While it does, feed.Hits() is 1, // and a second client gets 409. ``` Each request gets a stream of its own, from the first event: a mock serves many clients at once, such as every open tab of a dashboard, each with its own delays and its own end. --- Source: https://vlatte.dev/sse/reconnecting.md # Reconnecting A client such as `EventSource` reconnects when a stream ends or its connection drops: it waits the `retry` time the stream set, and sends the id of the last event it got as the `Last-Event-ID` header. A mock serves that reconnection like any request: resume the stream where it was, answer with another mock, or tell the client to stop. **Go** ```go mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().ID("1").Data("a"), mockhttp.NewEvent().ID("2").Data("b"), mockhttp.NewEvent().ID("3").Data("c"), ).Retry(3 * time.Second).ResumeFromLastEventID()) ``` **Mock file** ```yaml response: status: 200 event_stream: retry: 3s # sent first: how long clients wait before they reconnect resume: true # a Last-Event-ID gets only the events after that id events: - id: "1" data: a - id: "2" data: b - id: "3" data: c ``` ## Retry `EventStream.Retry(d)`, `retry` beside `events`, sends a `retry` field before the first event: how long the client waits before it reconnects. An event's own `retry` changes it from that event on. Clients read it in whole milliseconds, so `1.5s` goes as `retry: 1500`. ## Resuming With `ResumeFromLastEventID()`, `resume: true`, a request with a `Last-Event-ID` header gets only the events after the one with that id: above, `Last-Event-ID: 1` gets `b` and `c`. An id no event has gets them all, and so does a request without the header. The stream's `retry` is sent either way. Only ids set with `ID`, or `id` in a mock file, count: an `id` line inside a [raw event](https://vlatte.dev/sse/raw-events.md) is sent, but resuming doesn't know it. ## Answering a reconnection with another mock Match the `Last-Event-ID` header to give a reconnection another answer, such as the rest of a stream, or a different one: **Go** ```go m.MustMock(mockhttp.Getf("/jobs/7/progress"). Header(mockhttp.HeaderLastEventID, match.Equal("3")). Reply(mockhttp.NoContent())) ``` **Mock file** ```yaml request: method: GET path: /jobs/7/progress header: last-event-id: "3" response: status: 204 ``` ## Telling clients to stop A stream that ends makes `EventSource` reconnect, even when the server is done. Answer that reconnection with `204 No Content`, a reply without `event_stream`, as above, and the client stops. An answer that isn't a stream also stops it: a status other than `200` after redirects, or a `Content-Type` other than `text/event-stream`. A mock that answers `401` to a request without credentials, before any stream starts, makes `EventSource` give up rather than retry. ## Testing a reconnection Cut the first stream with [`fault.LimitData`](https://vlatte.dev/http/faults.md), so the connection drops after the events a client should keep, and resume the stream on the reconnection: **Go** ```go stream := mockhttp.NewEventStream( mockhttp.NewEvent().ID("1").Data("a"), mockhttp.NewEvent().ID("2").Data("b"), mockhttp.NewEvent().ID("3").Data("c"), ).Retry(1500 * time.Millisecond).ResumeFromLastEventID() // The first connection drops after the retry field and event 1. m.MustMock(mockhttp.Getf("/events").Once(). Pipe(fault.LimitData(len("retry: 1500\n\nid: 1\ndata: a\n\n"))). Reply(mockhttp.OK().EventStream(stream))) // The reconnection gets events 2 and 3: nothing is lost, nothing sent twice. m.MustMock(mockhttp.Getf("/events"). Header(mockhttp.HeaderLastEventID, match.Equal("1")). Reply(mockhttp.OK().EventStream(stream))) ``` **drop.yaml** ```yaml # The first connection drops after the retry field and event 1. request: method: GET path: /events repeat: 1 pipes: - type: limit_data bytes: 28 response: status: 200 event_stream: retry: 1500ms resume: true events: - id: "1" data: a - id: "2" data: b - id: "3" data: c ``` **resume.yaml** ```yaml # The reconnection gets events 2 and 3. request: method: GET path: /events header: last-event-id: "1" response: status: 200 event_stream: retry: 1500ms resume: true events: - id: "1" data: a - id: "2" data: b - id: "3" data: c ``` The client must reconnect after the drop with `Last-Event-ID: 1`, and end with `a`, `b` and `c`, once each. --- Source: https://vlatte.dev/sse/raw-events.md # Testing a client's parser Servers in the wild send streams the fields of an event can't write: CRLF or CR line endings, a byte order mark, fields without a space after the colon. A raw event sends its text as it is, to test that a client reads them as the [standard](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation) says. **Go** ```go mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Raw("\uFEFFdata: bom\r\n\r\n"), mockhttp.NewEvent().Raw("event: cr\rdata:no space\r\r"), mockhttp.NewEvent().Raw(": comment\nunknown: x\nretry: soon\ndata: kept\n\n"), mockhttp.NewEvent().Raw("id: 7\n\n"), )) ``` **Mock file** ```yaml response: status: 200 event_stream: events: - raw: "\uFEFFdata: bom\r\n\r\n" # a byte order mark, CRLF - raw: "event: cr\rdata:no space\r\r" # CR, no space after the colon - raw: ": comment\nunknown: x\nretry: soon\ndata: kept\n\n" # skipped lines - raw: "id: 7\n\n" # an id without data ``` A client that follows the standard dispatches three events, `bom`, `no space` of type `cr`, and `kept`, ignores the unknown field and the `retry` that isn't a number, and keeps `7` as its last event ID without dispatching anything. ## Raw events `Raw(text)`, `raw` in a mock file, sends its text in place of the event's fields. It takes a `delay`, and no other field: a raw event with one fails the mock. Its text needs the blank line that ends an event, unless the test is of an event that never ends. - **Templates.** A raw event is never a template, in a templated stream too. - **Ids.** An `id` line in a raw event is sent, and clients read it, but [resuming](https://vlatte.dev/sse/reconnecting.md#resuming) doesn't know it: it reads only the ids set with `ID` or `id`. - **Byte order mark.** Clients drop one only as the first bytes of the stream: not after the stream's `retry`, which is sent first, or a heartbeat. ## Cut connections and other faults [Pipes](https://vlatte.dev/http/pipes.md) and [faults](https://vlatte.dev/http/faults.md) apply to a stream as to any body, after the stream renders it: | Fault | Tests a client against | |---|---| | `fault.LimitData(n)`, or the `limit_data` fault with `bytes: n` | A connection that drops after `n` bytes, as a flaky network does, such as in the middle of an event: see [Testing a reconnection](https://vlatte.dev/sse/reconnecting.md#testing-a-reconnection). | | `fault.Latency(latency, jitter)` | Events that arrive late, at the pace they were sent. | | `fault.Bandwidth(bytesPerSecond)` | A slow link, which sends a large event in pieces. | | `fault.Slicer(averageSize, sizeVariation, delay)` | Events split across reads at random. | | `fault.ConnectionResetByPeerAfter(d)` | A reset `d` after the stream began. | A fault that holds the whole body, such as `fault.ChunkedDribbleDelay`, waits for a stream that keeps open forever: use it only on a stream that ends. ## Response mappers A [response mapper](https://vlatte.dev/http/mocks.md), `Map` in Go, reads the whole body, which a stream may never end: a mock with both fails to build. A stream inside a [sequence or random reply](https://vlatte.dev/http/replies.md#sequences) fails at the request it answers instead. --- Source: https://vlatte.dev/sse/other-features.md # With other features An event stream is a reply body, so the features of HTTP mocks work with it. This page covers where a stream behaves differently, or needs a setting. ## Proxying A [proxy reply](https://vlatte.dev/http/proxying.md), `From(target)` in Go or `proxy` in a mock file, passes a target's event stream on event by event, as the target sends it, not once it ends: the client gets the first event while the target hasn't sent the second. The server flushes each part of an answer whose `Content-Type` is `text/event-stream`, or whose length isn't known. A proxy reply with [response mappers](https://vlatte.dev/http/mocks.md), which read the whole body, fails on a target's stream at once. ## Recording A [recording](https://vlatte.dev/recording/index.md) keeps a proxied stream as a plain `body`, with its headers: ```json { "id": "GET-events-cc9d4ecf7d4bb412", "name": "GET /events", "request": {"method": "GET", "url": "/events"}, "response": { "status": 200, "header": {"Cache-Control": ["no-cache"], "Content-Type": ["text/event-stream"]}, "body": "id: 1\ndata: a\n\nid: 2\ndata: b\n\n" } } ``` Played back, it sends the whole stream at once, without the target's pauses, and ends: it doesn't keep open or resume. To play it back event by event, turn the `body` into an `event_stream`, with the `delay`s and `keep_open` it needs. ## Browsers and CORS A page's `EventSource` on another origin reads a stream only when the answer allows its origin. With `{withCredentials: true}`, it sends its cookies, and the answer must allow credentials and name the origin, not `*`: **Go** ```go m := mockhttp.MustNew(mockhttp.Setup().CORS(mockhttp.CORS(). AllowOrigin("https://app.example"). AllowCredentials(true))) m.MustMock(mockhttp.Getf("/events"). Cookie("session", match.Equal("s1")). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream(mockhttp.NewEvent().Data("hi")).KeepOpen()))) ``` **CLI** ```sh vlatte --cors-allowed-origin=https://app.example --cors-allow-credentials ``` See [CORS](https://vlatte.dev/http/cors.md) for the settings. ## The admin API and MCP `POST /mocks` on the [admin API](https://vlatte.dev/admin-api/index.md), and the `add_mock` tool of [MCP](https://vlatte.dev/mcp/tools.md), take a mock with an `event_stream`, as a mock file writes it, in JSON or YAML: ```sh vlatte --admin-addr=127.0.0.1:8081 curl -X POST -H 'Content-Type: application/json' \ -d '{"request":{"method":"GET","path":"/ticks"},"response":{"status":200,"event_stream":{"events":[{"data":"1"},{"data":"2"}]}}}' \ http://127.0.0.1:8081/mocks ``` `GET /mocks/{id}` describes a stream built in Go as a mock file would, but for `EventDelayFunc`, which a file can't write. The [mock file schema](https://vlatte.dev/reference/schemas.md) describes `event_stream`, so editors complete it. ## Feeds per user A [path template](https://vlatte.dev/http/matching.md#path-templates) and a templated stream serve each user the feed the path names, and a mock with a higher priority number, `Priority(1)` or `priority: 1`, answers what the first doesn't match, a request without credentials, before any stream starts: **Go** ```go m.MustMock(mockhttp.Request().Method(http.MethodGet). URLPathTemplate("/users/{id}/notifications"). Header(mockhttp.HeaderAuthorization, match.HasPrefix("Bearer ")). Reply(mockhttp.OK().EventStream(mockhttp.NewEventStream( mockhttp.NewEvent().Name("notification").ID("{{ .Request.PathParams.id }}-1"). Data(`{"user": "{{ .Request.PathParams.id }}", "text": "Your order shipped"}`), ).KeepOpen().Template()))) m.MustMock(mockhttp.Request().Method(http.MethodGet). URLPathTemplate("/users/{id}/notifications"). Priority(1). Reply(mockhttp.Unauthorized().PlainText("sign in first"))) ``` **Mock file** ```yaml request: method: GET path_template: "/users/{id}/notifications" header: authorization: [hasprefix, "Bearer "] response: status: 200 event_stream: keep_open: true events: - event: notification id: "{{ .Request.PathParams.id }}-1" data: '{"user": "{{ .Request.PathParams.id }}", "text": "Your order shipped"}' template: enabled: true ``` **Mock file, 401** ```yaml request: method: GET path_template: "/users/{id}/notifications" priority: 1 response: status: 401 header: content-type: text/plain body: sign in first ``` The `401` makes `EventSource` give up rather than retry: see [Telling clients to stop](https://vlatte.dev/sse/reconnecting.md#telling-clients-to-stop). --- Source: https://vlatte.dev/matchers/index.md # Matchers The `match` package holds the matchers every protocol shares. A mock's builder methods, such as `Header`, `Query` or `Field`, take one for the part of the request they read; [request journal](https://vlatte.dev/more/journal.md) queries take them too. ```go mockhttp.Getf("/users").Query("page", match.GreaterThan(0)) mockgrpc.Method("users.Users/Get").Field("id", match.HasPrefix("u-")) ``` In a mock file, a matcher is a list whose first item names it and whose other items are its arguments: `[contains, x]`, `[jsonpath, $.a, [gt, 1]]`. Names ignore case, so `hasPrefix` is `hasprefix`. Where a request's value goes, such as a header or a query parameter, text alone matches equal text: `page: "2"` is `page: [equal, "2"]`. ## How values compare Every matcher compares values by one rule, the same that mock files and settings read values by. A value has a class: nil, a bool, text, a number, a list or an object. - Values of one class compare by value. Numbers are equal when their exact values are, whatever their Go types: `10`, `int64(10)` and `10.0` are equal. - Most of a request is text: its path, query, headers and form values, and a body that isn't JSON. So `Equal`, and the matchers built on it, `IsIn`, `Contain` and `ItemsMatch`, read text as the class they compare with: - text compared with a number reads as a number when it is written in the JSON number grammar: `Equal(10)` matches the query value `"10"` and `"1e1"`, but not `"010"`, `"+10"` or `" 10"`; - anything compared with a bool reads as one the way `strconv.ParseBool` reads text: `Equal(true)` matches `true`, `"true"`, `"t"`, `"1"` and the number `1`. - Nothing else crosses classes: text never equals a list. `StrictEqual` and `EqualJSON` read no text at all. The [package docs](https://pkg.go.dev/github.com/caffeine-projects/vlatte/match) state the rule in full. | Page | Matchers | |---|---| | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | Equality, text, presence and URLs. | | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md) | Comparisons of numbers, and of dates and times, with offsets and truncation. | | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | Length, items, keys, and every or some item. | | [JSON and XML](https://vlatte.dev/matchers/json-xml.md) | JSONPath, JSON equality and JSON Schema; XML equality and XPath; JWT claims and signature verification. | | [Combining](https://vlatte.dev/matchers/combining.md) | `All`, `Any`, `Not` and the rest, and the matchers only Go has. | | [Custom matchers](https://vlatte.dev/matchers/custom.md) | Matchers of your own, in Go and in mock files. | | [All matchers](https://vlatte.dev/matchers/reference.md) | Every matcher, its Go name and its mock-file name, in one table. | --- Source: https://vlatte.dev/matchers/equality-text.md # Equality and text Compare values, match text, check for missing values, and match URL paths or query strings. These matchers work in Go and in mock-file matcher lists. ## Equality | Go | Mock file | Matches | |---|---|---| | `Equal(v)`, `Eq` | `equal`, `eq` | A value equal to `v`, by the [shared comparison rule](https://vlatte.dev/matchers/index.md#how-values-compare). | | `StrictEqual(v)`, `EQS` | `equalstrict`, `eqs` | A value equal to `v`, reading no text. | | `EqualIgnoreCase(s)`, `EQI` | `equalignorecase`, `eqi` | Text equal to `s`, ignoring case. | | `IsIn(items)` | `isin` | A value equal to one of `items`: `[isIn, [10, 20]]`. | | `EqualBytes(b)` | `equalbytes` | Bytes equal to `b`, given as standard base64 in a mock file. | ## Text | Go | Mock file | Matches | |---|---|---| | `Contain(v)` | `contains` | Text holding `v`, a list holding an item equal to `v`, or an object holding the key `v`. | | `HasPrefix(s)`, `HasSuffix(s)` | `hasprefix`, `hassuffix` | Text that starts or ends with `s`. | | `GlobMatch(pattern)` | `glob` | Text a glob matches, such as `/users/*`. | | `RegExpr(expr)` | `regex`, `regexp` | Text a Go regular expression matches. | | `Trim(m)` | `trim` | Text that `m` matches once its spaces are trimmed. | | `ToLower(m)`, `ToUpper(m)` | `lowercase`, `uppercase` | Text that `m` matches once lowered or uppercased. | | `Split(sep, m)` | `split` | Text that `m` matches as the list of its parts: `[split, ",", [len, 3]]`. | ## Presence | Go | Mock file | Matches | |---|---|---| | `Present()` | `present` | A value that is there. `[not, present]` matches a header the request doesn't have. | | `Nil()` | `nil` | Nil. | | `Empty()` | `empty` | Nil, and text, a list or an object of no items. | | `Truthy()`, `Falsy()` | `truthy`, `falsy` | A value that reads as true, or false, as `strconv.ParseBool` reads text. | | `Anything()` | `anything` | Anything. | ## URLs | Go | Mock file | Matches | |---|---|---| | `URLPath(glob)`, `URLPathf`, `URLPathMatch(m)` | `urlpath` | The path of a URL. | | `RequestURI(uri)` | `requesturi` | A URL whose path and query are those of `uri`, the parameters in any order. | --- Source: https://vlatte.dev/matchers/numbers-dates.md # Numbers, dates and times Compare numeric values and timestamps, including request text that represents them. Date matchers can compare against fixed times or `now`, with offsets and truncation. ## Numbers | Go | Mock file | Matches | |---|---|---| | `GreaterThan(n)`, `GT` | `greater`, `gt` | A number, or text that reads as one, greater than `n`. | | `GreaterThanOrEqual(n)`, `GTE` | `greatereq`, `gte` | Greater than or equal to `n`. | | `LessThan(n)` | `less`, `lt` | Less than `n`. | | `LessThanOrEqual(n)`, `LTE` | `lesseq`, `lte` | Less than or equal to `n`. | ## Dates and times `[before, x]`, `[after, x]` and `[equalDateTime, x]` (`eqdt`) compare a date and time in RFC 3339, in ISO 8601 without a zone, or in an HTTP format. `x` is one of those, or `now`, and either may end with an offset: `"now +3 days"`, `"2026-01-01T00:00:00Z -2 weeks"`. Options: `truncate_expected` and `truncate_actual`, each one of `first minute of hour`, `first hour of day`, `first day of month`, `first day of next month`, `last day of month`, `first day of year`, `first day of next year` and `last day of year`; `truncate_last: true` to offset first; `actual_format` with a Go layout, such as `02/01/2006`; `local_time: true` to compare wall clocks. An `x` without a zone is local, as with `local_time`; a request's date and time without a zone, against an `x` with one, is read in the server's zone. In Go: `Before`, `After`, `EqualDateTime`, `BeforeNow`, `AfterNow` and `EqualDateTimeNow`, with typed options such as `ExpectedOffset(3, match.Days)`. ```yaml request: header: X-Date: [after, "now -1 hours"] query: until: [before, "now +15 days", {truncate_expected: first day of month}] ``` --- Source: https://vlatte.dev/matchers/lists-objects.md # Lists and objects Match collection sizes, items, keys and nested fields. Use these with request parts that contain several values, such as HTTP `Headers` or `Queries`, or with values a JSONPath selects. | Go | Mock file | Matches | |---|---|---| | `Len(n)` | `len`, `length` | Text of `n` bytes, or a list or an object of `n` items. | | `Each(m)` | `each` | A list whose items, or an object whose members, all match `m`. | | `Some(m)` | `some` | A list with an item that matches `m`. | | `Item(i, m)` | `item` | A list whose item at `i` matches `m`. | | `ItemsMatch(items)` | `itemsmatch` | A list holding `items` in any order, and nothing else. | | | `hasexactly` | A list of as many values as matchers, each matched by a matcher of its own, in any order: `[hasExactly, [[equal, "1"], [contains, "2"]]]`. In Go, `ItemsMatch` takes matchers as items. | | `HasKey(path)` | `haskey` | An object that has the key, or JSON path. | --- Source: https://vlatte.dev/matchers/json-xml.md # JSON and XML These matchers read JSON and XML from request bodies, gRPC messages or other values, and inspect JWT headers and claims. `SignedJWT` also verifies a token's signature and lifetime with public keys. | Go | Mock file | Matches | |---|---|---| | `JSONPath(path, m)`, `Field` | `jsonpath`, `field` | What a JSONPath selects, matched by `m`: `[jsonpath, $.id, [gt, 1]]`. | | `EqualJSON(doc, opts...)`, `EQJ` | `equaljson`, `eqj` | JSON equal to `doc`. Options loosen it: see [JSON](#json). | | `JSONSchema(schema, opts...)` | `jsonschema` | JSON that conforms to a JSON Schema. | | `EqualXML(doc, opts...)`, `EQX` | `equalxml`, `eqx` | An equal XML document, by what it means: see [XML](#xml). | | `XPath(expr, m, opts...)` | `xpath` | What an XPath 1.0 expression selects, matched by `m`. | | `JWT(m)` | `jwt` | A JWT whose header and claims `m` matches, without checking its signature: see [JWTs](#jwts). | | `SignedJWT(keys, m)` | `signedjwt` | The same, for a token signed by one of `keys` and within its lifetime. | ## JSON `[eqj, {...}]` matches JSON equal to the document given, in full. An object of options after it loosens that: `ignore_array_order: true` pairs list items up in any order, and `ignore_extra_elements: true` ignores members and list items the stub doesn't name. `[jsonschema, schema]` matches JSON that conforms to a JSON Schema, an object or JSON text, of draft 2020-12 unless `$schema` or `{draft: 7}` says otherwise; it loads no `$ref` outside the schema. In Go: `EqualJSON(doc, IgnoreArrayOrder(), IgnoreExtraElements())` and `JSONSchema(text, SchemaDraft(JSONSchemaDraft7))`. ```yaml request: method: POST path: /pets body: [all, [ [eqj, {name: Rex, tags: [a, b]}, {ignore_array_order: true, ignore_extra_elements: true}], [jsonpath, $.id, [regex, '^[0-9]+$']] ]] ``` ## XML `[eqx, '1']` matches an equal XML document, by what it means: attributes in any order, text trimmed, comments and indentation left out, names compared by namespace URI. Options: `ignore_child_order: true`, and `namespaces: strict` to compare prefixes too, or `none` to read no namespaces. `[xpath, expression, matcher]` applies the matcher to what an XPath 1.0 expression selects: nothing is null, one result is itself, more are a list, so `[some, m]` matches any of them; an element is its XML. `{namespaces: {t: "urn:todo"}}` binds a prefix of the expression. In Go: `EqualXML`, `IgnoreChildOrder`, `XMLNamespaces`, `XPath` and `XPathNamespace`. ## JWTs `[jwt, matcher]` decodes a JWT and matches `{"header": {...}, "claims": {...}}`, so `jsonpath` reads it. The value is the token or an `Authorization` header with it: `Bearer `, or any other scheme, is read past. ```yaml request: method: GET path: /orders header: Authorization: [jwt, [all, [ [jsonpath, $.claims.scope, [contains, orders:read]], [jsonpath, $.header.alg, [equal, RS256]] ]]] ``` `jwt` doesn't check the signature: a token anyone signed matches. `[signedjwt, keys, matcher]` matches only a token signed by one of `keys`, a JWK Set or a JWK, as an object or JSON text, or PEM public keys or certificates, such as the key a GitHub App registers. It refuses `alg: none` and HMAC algorithms, a `kid` none of the keys has (keys without a `kid`, such as PEM ones, are tried in turn), a key whose JWK names another `alg`, and a token whose `exp` is past or `nbf` future. A private key in `keys` fails the mock as it loads. ```yaml request: method: POST path: /app/installations/42/access_tokens header: Authorization: [signedjwt, "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n", [jsonpath, $.claims.iss, [equal, "123456"]]] ``` A matcher can't read the server's own keys, so to check tokens the server's [`jwt` templates](https://vlatte.dev/templating/index.md) sign, load the key from a file with `template.keys` and give `signedjwt` its public half. In Go: `JWT(m)` and `SignedJWT(keys, m)`, which panics when `keys` holds no public key. --- Source: https://vlatte.dev/matchers/combining.md # Combining Combine matchers with AND, OR, NOT or XOR. Each combined matcher tests the same value, such as a request header or a JSON field. ## Combining | Go | Mock file | Matches | |---|---|---| | `All(ms...)` | `all` | What every matcher matches: `[all, [[gt, 1], [lt, 9]]]`. | | `Any(ms...)` | `any` | What some matcher matches. | | `Both(a, b)`, `Either(a, b)`, `XOR(a, b)` | `both`, `either`, `xor` | What both, either, or exactly one of two matchers match. | | `Not(m)` | `not` | What `m` doesn't match. | ## In Go only | Matcher | Matches | |---|---| | `Function(fn)`, `Predicate(fn)` | What a function reports true for. | | `Peek(m, action)` | What `m` matches, running `action` on the value first, such as to log it. | To use a matcher of your own in mock files, register it: see [Custom matchers](https://vlatte.dev/matchers/custom.md). --- Source: https://vlatte.dev/matchers/custom.md # Custom matchers Create matchers from Go functions, or register named matchers to use in mock files, admin API requests and journal queries. ## In Go Any `vlatte.Matcher` works where a matcher goes. `match.Function(fn)` and `match.Predicate(fn)` make one from a function, and a mock's whole-request matchers get the request itself: | Protocol | Method | Gets | |---|---|---| | HTTP | `Matches(m)`, `RequestMatches(fn)` | The `*http.Request`. | | gRPC | `Matches(m)`, `CallMatches(fn)` | The `*mockgrpc.Call`. | ```go m.MustMock(mockhttp.Postf("/upload"). RequestMatches(func(r *http.Request) (bool, error) { return r.ContentLength > 2048, nil }). Reply(mockhttp.Status(http.StatusRequestEntityTooLarge))) ``` A predicate, such as `RequestMatches` or `CallMatches`, can't be described: the admin API can't show, save or store its mock. A matcher given to `Matches` that describes itself is described, under `request.custom`. ## In mock files A server reads mock files with its registry of matchers: the built-in ones, and the custom ones you give it. A custom matcher is built from its arguments when the mock loads, and its name works anywhere a matcher list does, in mock files, admin API mocks and request journal queries. `request.custom` lists matchers of the whole request: each gets the `*http.Request` (`Builder.Matches` in Go). A registry belongs to the servers you give it to; servers can share one, gRPC ones too, whose `request.custom` matchers get the `*mockgrpc.Call`. ```go registry := matchbuild.MustNew(matchbuild.WithMatcher("body-too-long", func(_ *matchbuild.Registry, args []any) (vlatte.Matcher, error) { if len(args) != 1 { return nil, errors.New("takes a length") } limit, ok := args[0].(int64) if !ok { return nil, errors.New("takes a length") } return match.Function(func(v any) (bool, error) { r, ok := v.(*http.Request) return ok && r.ContentLength > limit, nil }), nil })) m := mockhttp.MustNew(mockhttp.Setup().Matchers(registry)) ``` ```yaml request: custom: [[body-too-long, 2048]] response: status: 413 ``` `matchbuild.Registry` builds the matchers of mock files: `registry.BuildMatcher(spec)` builds one from its spec in Go, as a mock file does. --- Source: https://vlatte.dev/matchers/reference.md # All matchers The matchers of the `match` package, by their Go name and their name in mock files, which ignores case. `Equalf`, `StrictEqualf`, `EqualIgnoreCasef` and `Containf` take their expected value as a format and its arguments. A blank mock-file name means Go only. Each row links to the page that explains it. | Go | Mock file | Page | |---|---|---| | `Equal(v)`, `Eq` | `equal`, `eq` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `StrictEqual(v)`, `EQS` | `equalstrict`, `eqs` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `EqualIgnoreCase(s)`, `EQI` | `equalignorecase`, `eqi` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `IsIn(items)` | `isin` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `EqualBytes(b)` | `equalbytes` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Contain(v)` | `contains` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `HasPrefix(s)`, `HasSuffix(s)` | `hasprefix`, `hassuffix` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `GlobMatch(pattern)` | `glob` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `RegExpr(expr)` | `regex`, `regexp` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Trim(m)` | `trim` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `ToLower(m)`, `ToUpper(m)` | `lowercase`, `uppercase` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Split(sep, m)` | `split` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Present()` | `present` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Nil()` | `nil` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Empty()` | `empty` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Truthy()`, `Falsy()` | `truthy`, `falsy` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `Anything()` | `anything` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `URLPath(glob)`, `URLPathf`, `URLPathMatch(m)` | `urlpath` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `RequestURI(uri)` | `requesturi` | [Equality and text](https://vlatte.dev/matchers/equality-text.md) | | `GreaterThan(n)`, `GT` | `greater`, `gt` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md) | | `GreaterThanOrEqual(n)`, `GTE` | `greatereq`, `gte` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md) | | `LessThan(n)` | `less`, `lt` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md) | | `LessThanOrEqual(n)`, `LTE` | `lesseq`, `lte` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md) | | `Before(t)`, `BeforeNow()` | `before` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md#dates-and-times) | | `After(t)`, `AfterNow()` | `after` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md#dates-and-times) | | `EqualDateTime(t)`, `EqualDateTimeNow()` | `equaldatetime`, `eqdt` | [Numbers, dates and times](https://vlatte.dev/matchers/numbers-dates.md#dates-and-times) | | `Len(n)` | `len`, `length` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `Each(m)` | `each` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `Some(m)` | `some` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `Item(i, m)` | `item` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `ItemsMatch(items)` | `itemsmatch` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `ItemsMatch` with matchers | `hasexactly` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `HasKey(path)` | `haskey` | [Lists and objects](https://vlatte.dev/matchers/lists-objects.md) | | `JSONPath(path, m)`, `Field` | `jsonpath`, `field` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md) | | `JWT(m)` | `jwt` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md#jwts) | | `SignedJWT(keys, m)` | `signedjwt` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md#jwts) | | `EqualJSON(doc, opts...)`, `EQJ` | `equaljson`, `eqj` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md) | | `JSONSchema(schema, opts...)` | `jsonschema` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md) | | `EqualXML(doc, opts...)`, `EQX` | `equalxml`, `eqx` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md) | | `XPath(expr, m, opts...)` | `xpath` | [JSON and XML](https://vlatte.dev/matchers/json-xml.md) | | `All(ms...)` | `all` | [Combining](https://vlatte.dev/matchers/combining.md) | | `Any(ms...)` | `any` | [Combining](https://vlatte.dev/matchers/combining.md) | | `Both(a, b)`, `Either(a, b)`, `XOR(a, b)` | `both`, `either`, `xor` | [Combining](https://vlatte.dev/matchers/combining.md) | | `Not(m)` | `not` | [Combining](https://vlatte.dev/matchers/combining.md) | | `Function(fn)`, `Predicate(fn)` | | [Combining](https://vlatte.dev/matchers/combining.md#in-go-only) | | `Peek(m, action)` | | [Combining](https://vlatte.dev/matchers/combining.md#in-go-only) | --- Source: https://vlatte.dev/templating/index.md # Templating Replies can render parts of the response from the request: header values, the body, and the name or content of a body file. So can multipart parts and webhooks. ```yaml request: method: POST path: /users response: status: 201 header_template: location: '/users/{{ .Request.Header.Get "X-User-Id" }}' body: '{"id": {{ uuid | toJSON }}, "name": {{ (fromJSON .Request.Body.String).name | toJSON }}}' template: enabled: true ``` In Go: ```go m.MustMock(mockhttp.Postf("/users").Reply(mockhttp.Created(). HeaderTemplate("Location", `/users/{{ .Request.Header.Get "X-User-Id" }}`). BodyTemplate(`{"id": {{ uuid | toJSON }}}`))) ``` ## What is a template Nothing else renders: `{{` anywhere else is text. - `header_template` values (`HeaderTemplate`). - With `template.enabled`: a text `body` (`BodyTemplate`), the content of a `body_file` (`BodyFileWithTemplate`), the text of `multipart` parts (`Template()`), and the text of `event_stream` [events](https://vlatte.dev/sse/events.md#templated-events) (`Template()`). - `body_file` names, always. The content of the file is written as it is unless `template.enabled` is set. - Webhook parameters: `url_template`, `header_template`, `body_template` and `multipart_template`. ## Data Templates read: | Field | What it holds | |---|---| | `.Request.Method`, `.Request.URL`, `.Request.URLPathSegments`, `.Request.Header`, `.Request.ID` | The request. `index .Request.URLPathSegments 0` is the first segment of the path. | | `.Request.PathParams.` | The parameters of the mock's [path template](https://vlatte.dev/http/mock-files.md), decoded. | | `.Request.Body.String`, `.Request.Body.Content`, `.Request.HasBody` | The body. JSON bodies are text: read their fields with `fromJSON`. | | `.Request.Cookie "name"`, `.Request.PostForm.Get "f"`, `.Request.FormFile "f"` | Cookies and forms. | | `.Server.URL` | The server's base URL. | | `.KV.Get "key"`, `.KV.Has "key"` | The server's [key-value store](#key-value-store). | | `.Data` | The map set with `m.SetData`. | | `.Provided.` | What `Setup().TemplateDataProviders(providers)` provide. | | `.Extra` | The reply's `template.data`, or `SetTemplateData` in Go. Webhooks don't have it. | | `.Scenarios.State "name"` | The state of a [scenario](https://vlatte.dev/scenarios/index.md). | | `.Scenarios.StateOf "name" "key"` | The state of the instance of a [scenario](https://vlatte.dev/scenarios/index.md) of a key. | ## Key-value store `.KV` reads the server's key-value store: `{{ .KV.Get "token" }}` writes the value of `token`, and `{{ if .KV.Has "token" }}` checks for a key that may be missing. Go code sets the values, as in `m.KV().Set(ctx, "token", []byte("abc123"))`, and so does the [admin API](https://vlatte.dev/admin-api/index.md), `PUT /kv/token` with `{"value": "abc123"}`, while the server runs. The server keeps them in memory, and `Setup().KV(store)` gives it a store of your own instead, such as one on Redis, that implements `vlatte.KV`: ```go type KV interface { Get(ctx context.Context, k string) ([]byte, error) Has(ctx context.Context, k string) (bool, error) Set(ctx context.Context, k string, v []byte) error Del(ctx context.Context, k string) error Keys(ctx context.Context) ([]string, error) } ``` `Get` of a key that isn't stored returns an error that wraps `vlatte.ErrKVKeyNotFound`, `Has` is true for a key set to an empty value, `Del` of a key that isn't stored is not an error, and `Keys` lists the keys in any order. A store keeps and returns copies of the values. A server reads the store from every request at once, so it must be safe for concurrent use. A store reads with the context of the request, or with an event's own for webhooks, which outlives a client that leaves. A CSV file can fill the store. `vlatte.NewCSVKV("testdata/kv.csv")` loads one, and `Setup().KV(kv)` gives it to a server. The `vlatte` command loads the file that `--kv-file`, `VLATTE_KV_FILE` or `kv.file` names, into the one store its HTTP and gRPC servers read. A server built in Go ignores `kv.file`. ```csv key,value token,abc123 greeting,"Hello, ""world""" banner,"line one line two" empty, ``` The first row is the header `key,value`, and each other row is a key and its value, quoted as RFC 4180 says, so `{{ .KV.Get "greeting" }}` renders `Hello, "world"`. A byte order mark, which Excel writes, is dropped, and keys and values keep their spaces. A missing header, a row without two fields, an empty key or a repeated key fails the load, and the error names the line. `Set` and `Del` change the store, never the file. The admin API's `POST /kv/reload` loads the file again, in place of every value: values set since are gone, and keys removed since come back. A store of your own that implements `vlatte.KVReloader` reloads too. The image's working directory is `/vlatte`, so mount the file there: `docker run -v "$PWD/kv.csv:/vlatte/kv.csv" -e VLATTE_KV_FILE=kv.csv …`. A store on Redis, with [go-redis](https://github.com/redis/go-redis), as [`kv_test.go`](https://github.com/caffeine-projects/vlatte/blob/main/mockhttp/e2e/kv_test.go) runs it: ```go type redisKV struct{ c *redis.Client } func (kv redisKV) Get(ctx context.Context, k string) ([]byte, error) { b, err := kv.c.Get(ctx, k).Bytes() if errors.Is(err, redis.Nil) { return nil, fmt.Errorf("%w: %q", vlatte.ErrKVKeyNotFound, k) } return b, err } func (kv redisKV) Has(ctx context.Context, k string) (bool, error) { n, err := kv.c.Exists(ctx, k).Result() return n == 1, err } func (kv redisKV) Set(ctx context.Context, k string, v []byte) error { return kv.c.Set(ctx, k, v, 0).Err() } func (kv redisKV) Del(ctx context.Context, k string) error { return kv.c.Del(ctx, k).Err() } func (kv redisKV) Keys(ctx context.Context) ([]string, error) { var keys []string it := kv.c.Scan(ctx, 0, "*", 0).Iterator() for it.Next(ctx) { keys = append(keys, it.Val()) } return keys, it.Err() } ``` `Setup().KV(redisKV{redis.NewClient(&redis.Options{Addr: "localhost:6379"})})` plugs it in. `Keys` lists every key of the database, so give the store a database of its own. `.Data` holds values too: a map of any values that `m.SetData` sets on one server. The key-value store holds text by key, and a store of your own can share it with other servers and processes. ## Functions Besides those of Go templates, such as `if`, `range`, `eq`, `len`, `index`, `slice`, `printf`, `html` and `urlquery`, templates call the functions below. The value a function works on comes last, so it pipes: `{{ .Request.Header.Get "X-Name" | trimSpace | toUpper }}`. List functions take the list first, as Go's `append` does. A bad argument, such as an unknown algorithm or a division by zero, fails the template. | Kind | Functions | |---|---| | JSON | `toJSON v`, `fromJSON text`, `JSONPath path v` (the first value a JSONPath selects in a value or JSON text, or nil), `JSONPathAll path v`, `deleteJSONPath path v`, `mergePatch patch v` ([RFC 7386](https://www.rfc-editor.org/rfc/rfc7386)); each `v` and `patch` is a value or JSON text, so a string is JSON text; `indentJSON text`, `compactJSON text` | | Strings | `toUpper`, `toLower`, `trimSpace`, `title`, `upperFirst`, `truncate max s`, `replaceAll old new s`, `split sep s`, `join sep list`, `substring start end s` (end -1 is the end), `slug`; they count runes, not bytes | | Regexps | `regexpFind pattern s`, `regexpSubmatch pattern s`, `regexpReplaceAll pattern repl s` (`$1` is a group), `regexpMatch pattern s`, in Go's syntax | | Values | `contains item collection` (a substring, a list item or a map key), `default fallback v` (when v is nil, `""` or empty; 0 and false are values) | | Numbers | `add`, `sub`, `mul`, `div`, `mod` (ints when both operands are integers, as in Go, so `div 7 2` is 3; float64 otherwise), `toInt v` and `toFloat v`, which convert a numeric string, such as a query parameter: arithmetic takes numbers only | | Lists | `list items...`, `append list item`, `insert list index item`, `remove list index` (-1 is the last), `randomItem list`, `shuffle list`, `reverse list`, `sortBy list path` (by what a JSONPath selects in each item, `"$"` for plain values; stable, missing values first). They return new lists. | | XML | `xpath expr xml` (the text of what an XPath 1.0 expression selects, or the number or boolean it computes), `xpathNodes expr xml` (each node's `.Name`, `.Text` and `.Attrs`; printing an element writes its XML) | | Time | `now`, `formatTime layout t` and `parseTime layout s` (a Go layout, `RFC3339` and the other names of package `time`, `unix` or `unixMilli`), `timeAdd offset t` (`"3 days"`, `"-1 month"` or a Go duration such as `"1h30m"`), `inZone zone t`, `truncateTime point t` (`"first day of month"`, `"last day of year"`, ...) | | Encoding | `base64Encode`, `base64Decode`, `queryUnescape`, `parseQuery text` (form text as `url.Values`: `(parseQuery $s).Get "a"`) | | Random | `uuid`, `randomString kind length` (`alphanumeric`, `alphabetic`, `numeric`, `alphanumericSymbols` or `hex`), `randomInt min max` and `randomFloat min max` (in [min, max)). Not for secrets. | | Hashes | `hash alg encoding s` (`md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224` to `sha3-512`; `hex` or `base64`), `hmac alg key encoding s` (`sha1`, `sha256` or `sha512`), as webhook providers sign payloads: `{{ hmac "sha256" "secret" "hex" .Request.Body.String }}` | | Tokens | `jwt kid claims` signs a JWT; `jwks kids...` publishes public keys: see [Tokens](#tokens). | | Environment | `env name`: see below | ```yaml response: body: | {"id": {{ JSONPath "$.order.id" .Request.Body.String | toJSON }}, "expires": {{ now | timeAdd "30 days" | formatTime "RFC3339" | toJSON }}, "items": {{ JSONPathAll "$.order.items[*].sku" .Request.Body.String | len }}} template: enabled: true ``` ## Tokens `jwt` signs a JWT with a private key the server holds, and `jwks` publishes its public half as a JWK Set, for a client to verify the signature. Use them to mock an authentication provider, or use the [OpenID Connect recipe](https://vlatte.dev/recipes/oidc.md) for the authorization code flow. The default template engine has no signing keys. Add them with `template.keys` in `.vlatterc`: ```yaml template: keys: - test-1=RS256 ``` The same setting is `--template-keys=test-1=RS256`, `VLATTE_TEMPLATE_KEYS=test-1=RS256`, or `mockhttp.Setup().TemplateKeys("test-1=RS256")` in Go. For gRPC, use `grpc.template.keys`, `--grpc-template-keys`, `VLATTE_GRPC_TEMPLATE_KEYS`, or `mockgrpc.Setup().TemplateKeys(...)`. Each entry names a key ID and its algorithm: | Entry | What it does | |---|---| | `test-1=RS256` | Generates a new private key when the server is created. A new server gets a different key, even with the same ID. | | `test-1=RS256:keys/test.pem` | Reads a PEM private key, with a relative path starting at `root_dir` (`grpc.root_dir` for gRPC). Use a file to keep the same key across restarts, or to give a signature matcher its public half. | Supported algorithms are `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512` and `EdDSA`. The key must fit the algorithm. HMAC algorithms and `none` are refused. Private keys stay in the engine: templates can publish only their public halves. A mock can then issue a signed token: ```yaml request: method: POST path: /token response: status: 200 header: content-type: application/json body: | {"access_token": {{ jwt "test-1" (fromJSON `{"sub":"ana"}`) | toJSON }}} template: enabled: true ``` `jwt` takes claims as JSON object text or a map, such as `fromJSON` returns. It signs exactly those claims: it adds no `iss`, `aud`, `iat` or `exp`, so set the claims the client validates. Its header contains the key's `alg`, its `kid`, and `typ: JWT`. `toJSON` above writes the returned token as a quoted JSON string. `{{ jwks "test-1" }}` renders a JWK Set as JSON text. Use it as the body of a `/jwks` mock with `Content-Type: application/json`, without quotes or `toJSON`. Name several keys to publish them together, such as `{{ jwks "old" "new" }}` for a key rotation test. A literal key ID the engine doesn't hold fails the mock when its template compiles; an ID read from request data is checked when it renders. For an engine of your own, add a `crypto.Signer` private key with `vlatte.WithSigningKey(kid, alg, key)` when creating the `vlatte.GoTemplateEngine`, then pass it with `Setup().TemplateEngine(engine)`. Setting both `TemplateKeys` and `TemplateEngine` is an error. Signing replies and matching incoming tokens are separate operations. The [`jwt` matcher](https://vlatte.dev/matchers/json-xml.md#jwts) only decodes a token; `signedjwt` verifies its signature and lifetime with public keys you give it. It doesn't read the template engine's signing keys. ## Environment variables `env` reads only the variables the server's settings name, by exact name: `template.env` (`--template-env API_URL,TENANT`, `VLATTE_TEMPLATE_ENV`, or `Setup().TemplateEnv(...)` in Go) and `grpc.template.env` for gRPC mocks. Without it, templates read none, so a mock file can't print the server's secrets. A mock whose template names a variable that isn't allowed fails as it loads; a name read from the request is checked when it renders. An allowed variable that isn't set is `""`: `{{ env "TENANT" | default "acme" }}`. On an engine of your own, allow them with `vlatte.WithEnv`. Add your own functions to the engine: ```go engine := vlatte.MustNewGoTemplateEngine(vlatte.WithTemplateFuncs(map[string]any{"upper": strings.ToUpper})) m := mockhttp.MustNew(mockhttp.Setup().TemplateEngine(engine)) ``` ## Missing keys Missing keys are errors. `{{ .Extra.nmae }}` fails the request and names the key, instead of writing ``, and so does `.KV.Get` of a key that isn't set. Read an optional key with `index`, as in `{{ with index .Extra "nick" }}{{ . }}{{ end }}`, and an optional key of the key-value store with `.KV.Has` first. `vlatte.WithMissingKey` changes the rule for map keys. ## Escaping Templates are Go's `text/template`, so a body can be any format and nothing is escaped for you. Write JSON values with `toJSON`, and use `html` or `urlquery` where they fit. ## Errors Templates compile when the mock is built, so one that doesn't compile fails the mock. A body file whose content is a template compiles when a request reads it, since its name can depend on the request. A template that fails at a request answers as a [failure](https://vlatte.dev/http/troubleshooting.md#failures): the error status, `VLatte-Error: reply`, and a body that names the mock and the error. ## File names A body file name rendered from the request stays inside the directory written before the template: `files/{{ .Request.URL.Query.Get "f" }}` opens files in `files` only, and refuses `../` or an absolute path from the request. ## Header values Header values are written without the spaces and line breaks around them, as `net/http` writes headers, so a YAML block scalar works. A line break inside a rendered value fails the reply, or the webhook, so the request can't add a header. ## Webhooks Webhooks compile their templates when the mock is built, and render them each time they run, with the data of the request that completed the mock. ## Other engines A server compiles all its templates with one `vlatte.TemplateEngine`. vlatte ships `vlatte.GoTemplateEngine`; another engine, such as one for Handlebars, plugs in by implementing two methods: ```go type TemplateEngine interface { Compile(name, text string) (Template, error) } type Template interface { Execute(w io.Writer, data any) error // safe for concurrent use } ``` Pass it with `Setup().TemplateEngine(e)`. Templates then use its syntax everywhere, mock files included, as in `{{Request.Method}}` for Handlebars. [`template_handlebars_test.go`](https://github.com/caffeine-projects/vlatte/blob/main/mockhttp/e2e/template_handlebars_test.go) wraps [raymond](https://github.com/mailgun/raymond) in a few lines. Other engines read the same fields, maps and methods without arguments; methods with arguments, such as `.KV.Get "key"`, are Go template syntax. ## In gRPC A gRPC mock renders its messages with `Template(texts...)`, `template` in a [gRPC mock file](https://vlatte.dev/grpc/mock-files.md): each text is a template of a message, as JSON, rendered for each call, then read as the method's output type. It reads the same engine, functions and settings, with the call in place of the HTTP request: | Field | What it holds | |---|---| | `.Request.Message.` | The request, by the field names of the `.proto` file. | | `.Request.Header` | The call's metadata. | | `.KV`, `.Data`, `.Provided.` | As for HTTP: `m.SetData` sets `.Data`, and `Setup().TemplateDataProviders(providers)` provide `.Provided`. | | `.Scenarios.State "name"`, `.Scenarios.StateOf "name" "key"` | The state of a [scenario](https://vlatte.dev/scenarios/index.md). | ```go m.MustMock(mockgrpc.Method("greeter.Greeter/SayHello"). Reply(mockgrpc.Template(`{"message": "hi {{ .Request.Message.name }}"}`))) ``` `grpc.template.env` names the environment variables gRPC templates may read. --- Source: https://vlatte.dev/scenarios/index.md # Scenarios A scenario serves mocks in turn: each mock of a scenario matches while the scenario is in the state it requires, and moves the scenario to its new state when it is picked to answer. Every scenario starts in `started`. **HTTP** ```go m.MustMock( mockhttp.Postf("/orders").ID("create"). StartScenario("order").ScenarioStateWillBe("created"). Reply(mockhttp.Created()), mockhttp.Getf("/orders/1").ID("read"). ScenarioIs("order").ScenarioStateIs("created"). Reply(mockhttp.OK().JSON(map[string]any{"id": 1})), ) ``` **gRPC** ```go m.MustMock( mockgrpc.Method("orders.Orders/Create").ID("create"). StartScenario("order").ScenarioStateWillBe("created"). Reply(mockgrpc.JSON(`{"id": "1"}`)), mockgrpc.Method("orders.Orders/Get").ID("read"). ScenarioIs("order").ScenarioStateIs("created"). Reply(mockgrpc.JSON(`{"id": "1", "status": "CREATED"}`)), ) ``` **Mock file** ```yaml id: read scenario: name: order required_state: created new_state: read request: method: GET path: /orders/1 response: status: 200 body: '{"id": 1}' ``` gRPC mocks take part in scenarios the same way, and their mock files take the same `scenario` key. ## States A mock that requires no state matches in any state. A mock that requires a state no request moved the scenario to yet doesn't match: a typo in a scenario's name never matches every request. ## Turns The scenario moves before the mock answers, so the next request matches in the new state, and of two concurrent requests in one state only one gets the mock of that state. It moves even when the mock then doesn't answer, such as when its client leaves during the mock's `delay`. ## Names Names ignore case: `Checkout` is `checkout`. A scenario is listed under the name its first mock gives it. ## Keys A mock with a scenario key plays in an instance of the scenario for each key: tests that run at once against one server, or tenants of one API, each move their own. The key is a template that renders with the request, as reply templates do: ``ScenarioKey(`{{ .Request.Header.Get "X-Test-Id" }}`)`` in Go, `key` under `scenario` in a mock file. Each key starts in `started`; keys are exact, so `t1` is not `T1`. A key that renders empty plays in the shared instance, the one of mocks without a key, and a key that fails to render fails the request's matching. ```yaml scenario: name: checkout required_state: started new_state: paid key: '{{ .Request.Header.Get "X-Test-Id" }}' ``` ## Moving scenarios In Go, `m.Scenarios(ctx)` lists each scenario with its state, the states of its keyed instances that moved, the states its mocks name, flagging those no path of mock moves reaches from `started`, and which mock moves it where. `m.SetScenario(ctx, name, state)` moves a scenario, and refuses a state none of its mocks names with `matchscenario.ErrUnknownState`; a scenario no mock names takes any state. `m.ResetScenario(name)` starts every instance of a scenario over, and `m.ResetScenarios()` every scenario. `matchscenario.Key("t1")`, as the last argument of `SetScenario` and `ResetScenario`, picks the instance of a key. In the [admin API](https://vlatte.dev/admin-api/index.md), `GET /scenarios`, `PUT /scenarios/{name}`, `DELETE /scenarios/{name}` and `DELETE /scenarios` do the same, and `?key=t1` picks the instance of a key. Its `/scenario-board` page does it from a browser: it shows where each scenario and each of its keys is, flags the states no path of mock moves reaches from `started`, moves a scenario with a click, and follows the moves as requests make them. ## In templates, near misses and the journal - A request no mock matched names the scenario a mock waits on: `differs in scenario order needs created, is started`. - Templates read a scenario's state, the one its mock moved it to: `{{ .Scenarios.State "checkout" }}`, and the state of a key: `{{ .Scenarios.StateOf "checkout" (.Request.Header.Get "X-Test-Id") }}`. One mock can then answer in every state. - The [journal](https://vlatte.dev/more/journal.md) keeps the moves of each request's mock under `scenarios`, and [`GET /events`](https://vlatte.dev/admin-api/index.md) streams a `scenario` event for each move, by a request or by the methods and admin API above, in the order the moves happened. --- Source: https://vlatte.dev/recording/index.md # Recording Record a real service once, then test without it. A recording sends requests to the service, writes what it answered as [mock files](https://vlatte.dev/http/mock-files.md), and the server answers the same requests the same way from then on: in later test runs, in CI, and with no network. **HTTP** ```go err := m.StartRecording( mockhttp.RecordDir("testdata/recorded"), mockhttp.RecordTarget("https://api.example.com"), ) // ... requests to m reach the target, and are recorded ... res, err := m.StopRecording(ctx) // writes the mock files, then serves them ``` **gRPC** ```go err := m.StartRecording(mockgrpc.RecordTarget("localhost:50051"), mockgrpc.RecordDir("testdata/grpc_recorded")) // ... calls to m reach the target, and are recorded ... res, err := m.StopRecording(ctx) // writes the mock files, then serves them ``` **CLI** ```sh vlatte --record --record-target=https://api.example.com # Ctrl-C writes the recordings; the next run serves them vlatte ``` ## How it works 1. **Take.** With a target, the requests the recording takes go to the target before any mock. Without one, it takes what the server proxies already. Filters choose which requests it takes. 2. **Write.** Each answer becomes a mock file that matches its request, and no other, in `testdata/vlatte/_mocks_recorded` by default for HTTP, and `_mocks_recorded` under the gRPC root directory for gRPC. 3. **Play back.** Stopping the recording serves its files, and a server started later serves them too: they are mock files like any other, which you review and commit. | Page | What it covers | |---|---| | [HTTP](https://vlatte.dev/recording/http.md) | What is recorded, filters, playback, what a recording matches and answers. | | [gRPC](https://vlatte.dev/recording/grpc.md) | How gRPC calls, streams included, are recorded and played back. | | [CLI](https://vlatte.dev/recording/cli.md) | Recording from the command line and replaying on the next run. | | [Settings](https://vlatte.dev/recording/settings.md) | Recording configuration, headers and upstream TLS. | | [Admin API](https://vlatte.dev/recording/admin-api.md) | Starting, inspecting and stopping a recording while the server runs. | | [Files and secrets](https://vlatte.dev/recording/files-secrets.md) | Where recordings go, and keeping secrets out of them. | The [Record and replay](https://vlatte.dev/guides/record-and-replay.md) guide records on demand inside a test and replays the same request with the upstream stopped. --- Source: https://vlatte.dev/recording/http.md # Recording HTTP An HTTP server records what it proxies, or what it sends to a target, as mock files, then answers the same requests the same way, without the service. ```go m := mockhttp.MustNew() m.MustStart() t.Cleanup(m.Close) err := m.StartRecording( mockhttp.RecordDir("testdata/recorded"), mockhttp.RecordTarget("https://api.example.com"), mockhttp.RecordPaths("/v1/*"), ) // ... requests to m.URL("/v1/...") reach the target, and are recorded ... res, err := m.StopRecording(ctx) // writes the mock files, then serves them ``` ## What is recorded - With a target, the requests the recording takes go to the target before any mock, and its answers are recorded. The others go to the mocks, as usual. - Without one, the recording takes what the server proxies: the forward proxy (`Setup().Proxy()`) and proxy replies (`From`, the `proxy` key). A mock's own answer is recorded only with `RecordIncludeMocked(true)`. - Only an answer the client got whole is recorded. The failures vlatte answers, such as a target it can't reach, aren't. ## Filters `RecordMethods`, in any case, and `RecordPaths` choose the requests the recording takes. A path is a glob whose `*` matches `/` too, so `/v1/*` takes `/v1/a/b`. `RecordFilter` takes any request pattern, made as a mock is: ```go mockhttp.RecordFilter(mockhttp.Request().Header("X-Record", match.Equal("yes"))) ``` A filter that fails takes nothing: the request is served as usual, and the failure is logged. ## Playback - `StopRecording` waits until every call it took is written, then serves the recordings: the mock files load again, with the recording's directory, so the hits of file mocks count from 0. When `ctx` ends first, it returns ctx's error, and the recording keeps writing: another `StopRecording` waits for it again, and a `StartRecording` once it wrote everything ends it. Either serves it. A `StopRecording` that `Close` cuts short says the recordings aren't served. - A server started later serves them too. The default mock file patterns include `testdata/vlatte/_mocks_recorded`, and a server serves the recordings of its `record.dir`. - A server serves the recordings of the last recording `StopRecording` ended, not those of earlier ones in other directories. The same request recorded into two directories a server serves has one ID twice: `StopRecording` reports it, and the server serves what it served before. - A mock that proxies every request needs a priority number above 0 to let the recordings answer first: `AnyMethod().Priority(10).Reply(From(target))`. - A recording answers its request before any proxy, so a later recording without a target doesn't record that request again: it records what is new. A recording with a target records every request it takes. ## What a recording matches A recording answers the request it recorded, and no other: | Part | Matched | |---|---| | Method | Ignoring case. | | Path and query | As `url` matches: `url: /items?page=2`. | | Headers | Those `RecordRequestHeaders` names, by their first value, or their absence. A redacted one by its presence. No other header. | | Body | A JSON body as JSON, whatever its spacing and member order. A urlencoded or multipart form by all its text fields. Other text as it is, and no body as none. The media type of the `Content-Type` too. | | Files of a form | By name, content type and content, as `form_file` patterns. | Calls a recording can't tell apart are one mock, whose answers play in turn, then the last one again. The same answer every time is one. `RecordRepeats(vlatte.RecordFirst)` keeps the first answer only. A recording doesn't match what it can't read, and then takes any body: one that isn't UTF-8 text, or is larger than `MaxInlineBody`, or that `MaxBodyParsingLimit` cut, or that a parser of the server's config read, or the urlencoded form of a request other than POST, PUT and PATCH, which Go doesn't read. The forward proxy's host isn't recorded: one path on two hosts is one mock. ## What it answers The status, the headers but those the server sets (`Date`, `Content-Length` and the hop-by-hop ones) and `Set-Cookie`, the trailers, and the body as the client got it, after the pipes of the mock. - A text body up to `MaxInlineBody`, 1 MiB by default, is written in the mock file. Other bodies go to files next to it, named after their content, so equal bodies share a file. `RecordBodyFiles(true)` writes every body to a file. - A gzip body that fits in `MaxInlineBody` decoded is recorded decoded, with `encoding: gzip`, and compressed again when it is served, into bytes that may differ. So a gzip body with a header or trailer that holds for its bytes, a strong `ETag`, `Content-MD5`, `Digest`, `Content-Digest`, `Repr-Digest` or `Content-Range`, is kept as sent, as are other encoded bodies, with their `Content-Encoding`. - A larger body goes to a file as it streams, so it never sits in memory whole. When the disk is slower than the sender, the answer waits for it, a second in all at most. Past that, or past `MaxBodySize`, 1 GiB by default, the call isn't recorded, and the client still gets every byte. - A body file plays back streamed, without a `Content-Length`. - `RecordExcludeResponseHeaders` sets the headers left out, in place of `Set-Cookie`: name it among them to keep leaving it out, or name none to keep every header. `RecordTransform` changes each recording, as Go values, before it is written: ```go mockhttp.RecordTransform(func(r *mockhttp.RecordedMock) error { for i := range r.Responses { delete(r.Responses[i].Header, "X-Request-Id") } return nil }) ``` ## Status `RecordingStatus` counts the calls taken, the mocks written, and the calls dropped or that failed. A response never waits for the calls before it to be written: when too many wait, the next ones are dropped and counted. --- Source: https://vlatte.dev/recording/grpc.md # Recording gRPC Record gRPC calls, including streams, as mock files and replay them without the original upstream. ```go err := m.StartRecording(mockgrpc.RecordTarget("localhost:50051"), mockgrpc.RecordDir("testdata/grpc_recorded")) // ... calls to m reach the target, and are recorded ... res, err := m.StopRecording(ctx) // writes the mock files, then serves them ``` A gRPC server records as an [HTTP one](https://vlatte.dev/recording/http.md) does. With `RecordTarget`, the calls the recording takes go to the target before any mock; without one, it takes the answers of `From` mocks, and of every mock with `RecordIncludeMocked(true)`. `RecordMethods` names the methods it takes, as `Method` does, and `RecordRequestHeaders` the metadata its mocks match. A recording matches a call by its method, its messages and that metadata, and answers with the header, messages, trailer and status of the calls of that request in turn, as a `response_sequence`, then the last one again; `RecordRepeats(vlatte.RecordFirst)` keeps the first only. A bidi call matches by its method and metadata only, as bidi mocks do, and plays its messages back at once. `Setup().Record(...)`, the `grpc.record` section of the settings and the admin API's `/recording` routes record too, and a server serves the recordings of its `grpc.record.dir`, `_mocks_recorded` under its root directory by default. --- Source: https://vlatte.dev/recording/cli.md # Recording from the CLI The `vlatte` command records a service until it stops, and serves the recordings on the next run: ```sh vlatte --record --record-target=https://api.example.com --record-paths='/v1/*' # Send the app's requests to the mock server, rather than directly to the API. # Ctrl-C writes the recordings into testdata/vlatte/_mocks_recorded. vlatte ``` Wait for the first command to finish writing before starting playback. Another Ctrl-C quits at once. Review the files for [secrets](https://vlatte.dev/recording/files-secrets.md#secrets) before committing them. `--grpc-record-*` flags record the gRPC server the same way. `vlatte --help` lists every flag, as does the [settings reference](https://vlatte.dev/reference/settings.md). For repeatable options, use [recording settings](https://vlatte.dev/recording/settings.md). To start or stop a recording while the server keeps running, use the [admin API](https://vlatte.dev/recording/admin-api.md). --- Source: https://vlatte.dev/recording/settings.md # Recording settings The `record` section of the configuration records from server start to close: ```yaml # .vlatterc.yaml record: target: https://api.example.com paths: - /v1/* request_headers: - Accept exclude_response_headers: - Set-Cookie - X-Request-Id ``` Setting a key in `record` turns recording on. `record: false` turns it off. The `grpc.record` section configures gRPC recording. See [Configuration](https://vlatte.dev/run/configuration.md) for precedence between code, files, environment variables and flags, and the [settings reference](https://vlatte.dev/reference/settings.md) for every recording key. In Go, `Setup().Record(...)` records from start to close too. For recording only part of a test, use `StartRecording` and `StopRecording`, as in the [record and replay guide](https://vlatte.dev/guides/record-and-replay.md). ## Upstream TLS The target's TLS certificate is verified: a recording sends the target its requests, credentials included. `record.skip_ssl_verify`, or `RecordSkipSSLVerify(true)`, skips verification, such as for a self-signed certificate. In Go, `RecordTransport(rt)` sends through a transport of yours instead, such as one that trusts the target's CA. That transport decides whether it verifies the target: setting `skip_ssl_verify` with it fails the recording. ## Files and headers `exclude_response_headers` replaces the default list, so include `Set-Cookie` to keep excluding it. Request headers named by `request_headers` become matching criteria in the recorded mocks. See [HTTP recording](https://vlatte.dev/recording/http.md#what-a-recording-matches) and [Files and secrets](https://vlatte.dev/recording/files-secrets.md) before saving credentials or personal data. --- Source: https://vlatte.dev/recording/admin-api.md # Recording through the admin API The [admin API](https://vlatte.dev/admin-api/index.md) starts and stops recordings while the server runs. Its `/recorder` page provides the same controls in a browser. ## Start a recording `POST /recording` takes the keys of the `record` settings section. Send the app's requests to the mock server after starting it: ```sh curl -X POST -H 'Content-Type: application/json' \ -d '{"target":"https://api.example.com","paths":["/v1/*"]}' \ http://127.0.0.1:8081/recording ``` Use the HTTP server's admin address for HTTP recording, and the gRPC server's admin address for gRPC recording. The CLI prints those addresses when it starts. ## Check progress `GET /recording` reports recording status, including how many calls were taken, written, dropped or failed: ```sh curl http://127.0.0.1:8081/recording ``` ## Stop and replay `DELETE /recording` stops the recording and lists the files it wrote: ```sh curl -X DELETE http://127.0.0.1:8081/recording ``` The server then serves those files. Review them for [secrets](https://vlatte.dev/recording/files-secrets.md#secrets) before committing them. The [OpenAPI reference](https://vlatte.dev/reference/admin-api.html){target="_blank" rel="noopener noreferrer"} describes the request and response formats. --- Source: https://vlatte.dev/recording/files-secrets.md # Files and secrets Find where recordings are written and how to keep credentials and sensitive payloads out of saved fixtures. ## Files A recording writes a mock file named after its request, such as `GET-items-page-2-.mock.json`, or `.yaml` or `.yml` with `RecordFormat`, into its directory: `{workdir}/_mocks_recorded` by default, `testdata/vlatte/_mocks_recorded`. A mock file appears whole, or not at all. ## Secrets Mock files are plain text, and often committed: review them first. They hold the request bodies recordings match, such as a password or a `client_secret`, the values of the headers `RecordRequestHeaders` names, the response headers and bodies, such as tokens, and the query, in the URL and in the file name. `Set-Cookie` is left out by default. The headers `Setup().RedactHeader(...)` names keep their values out too: a response header is written ``, and a request header matches by its presence. `RecordTransform` hides the rest. --- Source: https://vlatte.dev/verify/index.md # Verifying requests A test checks what the code under test sent: which requests came, how many, by when, and in what order. A server with a [journal](https://vlatte.dev/more/journal.md) keeps every request it gets, whatever answered it, and `m.VerifyRequests()` selects and counts them, on HTTP and gRPC servers alike. A failed check says why: the requests it selected and, when it wanted more, those that came closest, with the parts each one missed. ## Do you need it? Most tests don't. To check that a mock was called, use the scope that `MustMock` returns: it asserts the calls of the mocks it added, with no journal and no criteria to repeat. ```go paid := m.MustMock(mockhttp.Postf("/payments").Reply(mockhttp.Created())) refund := m.MustMock(mockhttp.Postf("/refunds").Reply(mockhttp.Created())) // ... the code under test sends its requests ... paid.AssertCalled(t) // every mock of the scope was called paid.AssertNumberOfCalls(t, 1) // and their calls add up to 1 refund.AssertNotCalled(t) // no mock of the scope was called ``` Request verification is for what a scope can't see: - requests no mock answered; - counts across several mocks, or by request parts no single mock matches on; - requests the code under test sends in the background, which a check waits for; - the order of requests, and a test's own requests on a shared server, with marks; - checks from outside Go, over the admin API or MCP. ## Checking the journal **HTTP** ```go m := mockhttp.MustNew(mockhttp.Setup().Journal("")) // "" keeps the journal in a temporary directory m.MustStart() t.Cleanup(m.Close) // ... the code under test sends its requests ... m.VerifyRequests().Method("POST").Path(match.Equal("/orders")).Times(t, 1) m.VerifyRequests().Outcome(journal.OutcomeUnmatched).Never(t) ``` **gRPC** ```go m := mockgrpc.MustNew(mockgrpc.Setup().Journal("")) m.MustStart() t.Cleanup(m.Close) // ... the code under test calls the server ... m.VerifyRequests().Path(match.Equal("/helloworld.Greeter/SayHello")). Body(match.JSONPath("$.name", match.Equal("bob"))). Times(t, 1) ``` **Admin API** ```sh curl -X QUERY -H 'Content-Type: application/json' \ -d '{"method": "POST", "path": "/orders", "count": 1}' \ http://127.0.0.1:8081/requests/verify # {"passed": true, "count": 1, "waited_ms": 0} ``` `journal` is `github.com/caffeine-projects/vlatte/journal`, and `match` is `github.com/caffeine-projects/vlatte/match`. The checks live in package `github.com/caffeine-projects/vlatte/verify`, which a server embeds when its setup keeps a journal: a test calls them on the server. ## How it works 1. **Select.** `VerifyRequests()` selects every request the journal holds. Criteria narrow the selection: the method, path, headers, body, status, outcome, the mock that answered, or the request part of a mock. 2. **Check.** `Times`, `AtLeast`, `AtMost`, `Never` and `CountIs` assert how many requests the selection holds, and `verify.InOrder` asserts the order of several selections. A check runs now, or waits for its count without polling. 3. **Report.** A failure reports with `t.Errorf` and returns `false`. It lists the requests the selection holds, and those that came closest, a line each, without headers or bodies, which may hold secrets. A check never passes on part of the requests: one whose requests the journal dropped or cleared fails with an error, rather than count what is left. | Page | What it covers | |---|---| | [Selecting requests](https://vlatte.dev/verify/selecting.md) | The criteria, request patterns, and how gRPC calls are selected. | | [Counts and reports](https://vlatte.dev/verify/counting.md) | The assertions, the failure report, and reading the requests. | | [Waiting](https://vlatte.dev/verify/waiting.md) | Checking a request the code under test sends in the background, or that none comes for a while. | | [Marks and order](https://vlatte.dev/verify/marks-and-order.md) | Counting a test's own requests on a shared server, and checking their order. | | [Admin API and MCP](https://vlatte.dev/verify/admin-api.md) | The same checks for a client in any language, or an AI agent. | | [Troubleshooting](https://vlatte.dev/verify/troubleshooting.md) | The errors of a check that can't run, and what to do about each. | The [Request journal](https://vlatte.dev/more/journal.md) page covers the journal itself: what each entry holds, where it is kept, and its limits. --- Source: https://vlatte.dev/verify/selecting.md # Selecting requests `m.VerifyRequests()` selects every request the journal holds, whatever its outcome. Each criterion narrows the selection, and a request must match them all: ```go orders := m.VerifyRequests(). Method("POST"). Path(match.Equal("/orders")). Header("Content-Type", match.Contain("json")). Body(match.JSONPath("$.item", match.Equal("tea"))) orders.Times(t, 1) ``` | Criterion | Selects the requests | |---|---| | `Method(methods...)` | Of any of the methods, in any case, such as `"POST"`. | | `Path(m)` | Whose path `m` matches: the URL's path for HTTP, the full method for gRPC. | | `Target(m)` | Whose full target `m` matches, such as the URL with its query string. | | `Header(name, m)` | Whose header `name`, in any case, `m` matches: its first value, or `""` for a request without it. Each name adds a header to match. gRPC calls have metadata. | | `Body(m)` | Whose body, as text, `m` matches, such as with `match.JSONPath`. | | `Status(m)` | Whose answer had a status `m` matches, an `int`: an HTTP status, or a gRPC code. | | `Outcome(outcomes...)` | Of any of the outcomes: `journal.OutcomeMatched`, `OutcomeUnmatched`, `OutcomeProxied`, `OutcomeFailed` or `OutcomeAbandoned`. | | `MockID(m)` | That a mock whose id `m` matches answered. | | `Matching(patterns...)` | That the request part of every pattern matches: see [Request patterns](#request-patterns). | The criteria take the [matchers](https://vlatte.dev/matchers/index.md) mocks take. They are the keys of the admin API's [queries](https://vlatte.dev/verify/admin-api.md#keys) too. ## A selection is a value Each method returns a new selection and leaves the one it was called on as it was. Name a selection once, and narrow it more than one way: ```go orders := m.VerifyRequests().Method("POST").Path(match.Equal("/orders")) orders.Times(t, 3) orders.Body(match.JSONPath("$.item", match.Equal("tea"))).Times(t, 1) orders.Status(match.Equal(http.StatusCreated)).Times(t, 3) ``` A criterion a selection can't take fails the check, never selects nothing: a check that selected nothing would pass `Never`. A `nil` matcher, `Method()` with no method, `Outcome()` with no outcome or with one that is none, and `Method` on a gRPC server each fail the check with why. ## Request patterns `Matching` takes the request part of the server's own mock builders, for what no criterion says, such as an HTTP query parameter, a form field or a file. The server rebuilds each request from the journal and matches it as a mock would, through its body parsers: ```go m.VerifyRequests(). Matching(mockhttp.Request().Query("page", match.Equal("2"))). Times(t, 1) m.VerifyRequests(). Matching(mockhttp.Request().FormField("plan", match.Equal("pro"))). AtLeast(t, 1) ``` A pattern's reply counts for nothing. A pattern with `Times` or `Once`, which count served requests, a scenario, which moves when its mock is picked, or a part the journal doesn't keep, such as `ClientIP`, fails the check. A rebuilt request has no `RemoteAddr`, nor TLS state for a `RequestMatches` predicate. Select by `Method` and `Path` next to a pattern when you can: the journal indexes them, so the pattern reads only the requests they leave. ## gRPC calls A gRPC server keeps the same journal with `mockgrpc.Setup().Journal(dir)`, and selects its calls the same way: - A call's path is its full method, such as `/helloworld.Greeter/SayHello`. It has no method, so select it by `Path`: `Method` fails the check with `verify.ErrNoMethod`. - Its headers are its metadata, and its status is its code, the one the client got. - Its body is its request message as JSON, which `match.Field` and `match.JSONPath` read. A client or bidi stream is one request, journaled as it ends, whose body is the JSON array of the messages its client sent. - Patterns are gRPC mock builders, such as `mockgrpc.Method("SayHello").Field("name", match.Equal("bob"))`. ```go m.VerifyRequests(). Path(match.Equal("/helloworld.Greeter/SayHello")). Header("authorization", match.HasPrefix("Bearer ")). Status(match.Equal(int(codes.OK))). Times(t, 1) ``` --- Source: https://vlatte.dev/verify/counting.md # Counts and reports An assertion counts the requests a selection holds, now or over a [wait](https://vlatte.dev/verify/waiting.md), and reports a failure with `t.Errorf`, as `testing.T` does, so the test goes on. Each returns `true` when it passed: | Assertion | Passes when the selection holds | |---|---| | `Times(t, n)` | Exactly `n` requests. | | `AtLeast(t, n)` | `n` requests or more. | | `AtMost(t, n)` | `n` requests or fewer. | | `Never(t)` | No request. | | `CountIs(t, m)` | A number of requests `m`, a matcher of an `int`, matches, such as `match.LTE(3)`. | ```go if !m.VerifyRequests().Path(match.Equal("/login")).Times(t, 1) { t.FailNow() // the rest of the test needs the login } m.VerifyRequests().Path(match.HasPrefix("/retry")).CountIs(t, match.All(match.GTE(1), match.LTE(3))) ``` `t` is a `vlatte.TestingT`, such as a `*testing.T`. An assertion runs with a context of its own, so one in a `t.Cleanup` works once the test's context is done. ## The failure report A failure says what the check expected and what it got. It lists the requests the selection holds, the newest first, ten at most. When the check wanted more requests than it got, it lists the requests of the journal that came closest too, ranked by the parts of the selection they missed, with the parts each missed: ```text Expected exactly 1 request(s) with method, path. Got 0. Closest requests (2 of 2 the server served, the newest first): #2 2026-10-29T14:05:55.699Z unmatched POST http://127.0.0.1:34207/order 418, closest [create-order] differs in path missed 1 of 2: path #1 2026-10-29T14:05:55.698Z matched PUT http://127.0.0.1:34207/orders 200 [update-order] missed 1 of 2: method ``` Each request has a line: its number in the journal, its time, outcome, method, URL and status, the id of the mock that answered it, and, for a request no mock matched, the mock that came closest and what differed. A line leaves out the headers and the body, which may hold secrets: the number and the request's id, from its `VLatte-Request-Id` header, get the rest of it from the journal. The closest requests come from the newest 100 of the journal. A check that wanted fewer requests lists the ones it selected only, as those are the ones that should not have come. ## Reading the requests `Find` returns the requests a selection holds, the newest first, with what the journal kept of each, and `Count` returns how many. Neither waits nor asserts: ```go orders := m.VerifyRequests().Method("POST").Path(match.Equal("/orders")) entries, err := orders.Find(ctx) n, err := orders.Count(ctx) ``` A selection has no limit, so `Find` returns every request it holds. `m.Journal().Find` takes a [`journal.Query`](https://vlatte.dev/more/journal.md) with a `Limit`, and pages through the requests instead. Like a check, `Find` and `Count` fail rather than return part of the requests: see [Troubleshooting](https://vlatte.dev/verify/troubleshooting.md). --- Source: https://vlatte.dev/verify/waiting.md # Waiting A request is journaled as the server ends its answer, as hits count. A client gets a small answer only then, so a check right after the call that sent the request finds it. A request the code under test sends in the background, such as a webhook, a retry or a call from a goroutine, may not have come yet when the test checks. Neither may a stream, or a large answer whose start the client reads first. Those checks wait. ## Within `Within(d)` waits up to `d` for the count to pass. It checks at once, then again as each request comes, and passes as soon as the count does. It fails when `d` ends: ```go // The code under test calls the payment webhook in the background. m.VerifyRequests().Path(match.Equal("/hooks/paid")).Within(5 * time.Second).Times(t, 1) ``` The wait wakes when the journal changes, rather than poll, so a check passes the moment its request comes, and a long `d` costs nothing when it does. A count that passes with no request, such as `Never`'s or `AtMost`'s, would pass `Within` at once, so it fails the check: use `During`. ## During `During(d)` holds the count for `d`. It fails as soon as the count fails, and passes when `d` ends. It tells that something did not happen in that time: ```go // No retry within a second of the failure. m.VerifyRequests().Path(match.Equal("/charges")).During(time.Second).AtMost(t, 1) m.VerifyRequests().Outcome(journal.OutcomeUnmatched).During(500 * time.Millisecond).Never(t) ``` ## What ends a wait - A clear of the journal, such as `POST /reset` of the admin API or `m.Journal().Clear()`, fails the check with `verify.ErrCleared`: the requests the check had counted are gone. - A server that closes fails it with `journal.ErrClosed`. A wait can't take part of a [check of an order](https://vlatte.dev/verify/marks-and-order.md#order): wait for the last step with its own check first, then check the order. --- Source: https://vlatte.dev/verify/marks-and-order.md # Marks and order Select requests after a mark and verify their order without counting traffic from earlier tests. ## Marks A check counts every request the journal holds. Tests that share one server, one after the other, see each other's requests: a test that checks `Times(t, 1)` after another test sent the same request fails with 2. `m.JournalMark()` takes a mark of the journal as it is now, and `Since(mark)` selects the requests that came after it, and none of those before: ```go func TestCheckout(t *testing.T) { mark := shared.JournalMark() // shared is a server the package's tests share // ... the code under test sends its requests ... shared.VerifyRequests().Since(mark).Path(match.Equal("/orders")).Times(t, 1) } ``` A mark isolates tests that run one after the other. Two tests that run at the same time on one server, with `t.Parallel()`, see each other's requests after their marks: give each one its own server, or select by a header that tells their requests apart. A journal kept in a named directory holds the requests of the server's earlier runs too, which a check without a mark counts. Tests use `Journal("")`, a temporary directory, or take a mark first. A mark belongs to its server's journal. A mark of another server fails the check with `verify.ErrOtherJournal`, and one the journal was cleared after with `verify.ErrCleared`. ## Order `verify.InOrder(t, steps...)` asserts that the server got a request of each step, each one after the request of the step before. Other requests may come between: ```go login := m.VerifyRequests().Method("POST").Path(match.Equal("/login")) order := m.VerifyRequests().Method("POST").Path(match.Equal("/orders")) logout := m.VerifyRequests().Method("POST").Path(match.Equal("/logout")) verify.InOrder(t, login, order, logout) ``` `verify` is `github.com/caffeine-projects/vlatte/verify`. For each step, `InOrder` takes the first of its requests after the request of the step before, so one request serves one step only. A failure names the step that had no request after the one before. Here the order came before the login: ```text Expected requests in this order: 1. request with method, path: #2 2. request with method, path: none after #2, #1 before it 3. request with method, path: not checked ``` An order is checked now. A step that waits, with `Within` or `During`, fails it: wait for the last step with its own check first, then check the order. Steps of different servers fail it too, as their journals don't share an order. Steps with `Since` count from their mark, as other checks do. --- Source: https://vlatte.dev/verify/admin-api.md # Admin API and MCP A client in any language, or an AI agent, checks the requests as a Go test does, through the [admin API](https://vlatte.dev/admin-api/index.md). `QUERY /requests/verify` takes a check in the body and answers its verdict, with the report a Go test gets. One engine runs both, so a check gives the same verdict from Go and from the admin API. A Go test that only checks its mocks were called needs none of this: see [Do you need it?](https://vlatte.dev/verify/index.md#do-you-need-it). ```sh curl -X QUERY -H 'Content-Type: application/json' \ -d '{"method": "POST", "path": "/orders", "count": 1, "within": "2s"}' \ http://127.0.0.1:8081/requests/verify # {"passed": true, "count": 1, "waited_ms": 12} ``` `QUERY` is the HTTP [`QUERY`](https://httpwg.org/specs/rfc10008.html) method, safe as `GET` is, with a body: `curl -X QUERY`. ## Keys A check takes the keys of `QUERY /requests`, with the matchers of mock files, to select the requests, and these to check them: | Key | What it does | |---|---| | `kind`, `outcome`, `mock_id`, `method`, `path`, `target`, `status`, `body`, `header`, `since`, `until`, `request` | Select the requests, as for [`QUERY /requests`](https://vlatte.dev/more/journal.md). `limit` and `before` don't apply: a check counts all it selects. | | `count` | The number of requests: a number, or a matcher list, such as `["gte", 2]` or `["lte", 3]`. None is at least 1. | | `within` | Waits up to this long for the count to pass, such as `"2s"`, as [`Within`](https://vlatte.dev/verify/waiting.md#within) does. | | `during` | Holds the count this long, such as `"2s"`, as [`During`](https://vlatte.dev/verify/waiting.md#during) does. | | `after` | Checks the requests after this mark only: see [Marks](#marks). | | `in_order` | A list of steps, each the selecting keys of a query, to check an [order](https://vlatte.dev/verify/marks-and-order.md#order) in place of a count. | A wait is at most 30 seconds, as the client holds its connection all along: check again for a longer one. `within` and `during` don't go together, nor `within` with a count that passes with no request, nor `in_order` with `count` or a wait. An order's steps take no `after` of their own: the check's `after` holds for each. ```sh curl -X QUERY -H 'Content-Type: application/json' \ -d '{"in_order": [{"path": "/login"}, {"path": "/orders"}, {"path": "/logout"}]}' \ http://127.0.0.1:8081/requests/verify # {"passed": true, "waited_ms": 0} ``` ## The verdict A check that ran answers `200`, whether it passed or not: a failed check is an answer, not an error. | Field | What it holds | |---|---| | `passed` | Whether the check passed. | | `count` | How many requests it selected. An order has none. | | `waited_ms` | How long it waited, in milliseconds. | | `report` | Why it failed, as a Go test's report says it. | | `selected` | The requests it selected, the newest first, ten at most. | | `closest` | When it wanted more requests, those that came closest, with `passed` and `total`, the parts each passed of those checked, and `missed`, the names of those it missed. | A listed request has its `seq`, its number in the journal, its `id`, and its `line`, without its headers or body: `GET /requests/{id}` answers the rest of it. ```json { "passed": false, "count": 0, "waited_ms": 0, "report": "\nExpected exactly 1 request(s) with method, path.\nGot 0.\n ...", "closest": [ {"seq": 2, "id": "c6f3…", "line": "#2 2026-10-29T14:05:55.699Z unmatched POST http://127.0.0.1:34207/order 418", "passed": 1, "total": 2, "missed": ["path"]} ] } ``` ## Marks A client takes a mark before its test, and sends it as `after`: the requests up to it are other tests'. `GET /requests` and `QUERY /requests` answer `mark`, the number of the newest request of the journal, and `GET /requests?limit=0` answers it alone: ```sh curl http://127.0.0.1:8081/requests?limit=0 # {"count": 41, "requests": [], "mark": 41} # ... the test sends its requests ... curl -X QUERY -H 'Content-Type: application/json' \ -d '{"path": "/orders", "after": 41, "count": 1}' \ http://127.0.0.1:8081/requests/verify ``` `after` works on `GET /requests` (`?after=41`) and `QUERY /requests` too, to list the requests after a mark. ## Errors A check that can't run answers a [problem document](https://vlatte.dev/admin-api/index.md), never a verdict, as a check that selected nothing could pass: | Status | Code | When | |---|---|---| | `400` | `invalid_body` | The check can't run: an unknown key, a matcher that doesn't exist, a negative count, a wait out of bounds, or keys that don't go together. | | `409` | `incomplete_journal` | The journal no longer holds every request the check counts: it dropped some to keep `journal.max_entries`, it was cleared after the mark or during the wait, the mark is past its newest request, or it cut a body the check reads. See [Troubleshooting](https://vlatte.dev/verify/troubleshooting.md). | | `501` | `not_supported` | The server keeps no journal. | | `503` | `closing` | The admin API or the mock server closed during the wait. | `POST /reset` clears the journal, so it ends a wait with `409`. ## MCP Over [MCP](https://vlatte.dev/mcp/setup.md), the `verify_requests` tool runs the same check. Its argument is the check under `check`, and it answers the verdict: ```json {"check": {"method": "POST", "path": "/orders", "count": 1, "within": "2s"}} ``` `list_requests` and `query_requests` take `after` too, and answer `mark`. A server without a journal has none of these tools. --- Source: https://vlatte.dev/verify/troubleshooting.md # Troubleshooting A check never passes or fails on part of the requests it counts. When it can't count them all, or can't run at all, it fails with an error that says why, which an assertion reports with `t.Errorf`, and `Find` and `Count` return. The errors are in package `verify`, unless named otherwise. ## The journal dropped requests ```text verify: the journal dropped requests the check counts: raise its MaxEntries: 1200 of them ``` `ErrDropped`. The journal keeps the newest 10 000 requests by default, and drops the oldest to keep that many. Requests the check would count may be among those dropped, so a count of the rest could pass when it shouldn't, or `Never` miss a request the server got. A check without a mark counts from the start of the journal, so on a server that has dropped requests it always fails this way. Either: - Take a mark before the test and select with `Since(mark)`: the check counts from the mark, and passes as long as the journal still holds what came after it. - Raise `journal.max_entries` (`--journal-max-entries`, or `JournalConfig.MaxEntries` in Go), or set it to `0` to keep every request. Over the admin API, it is a `409` with the code `incomplete_journal`. ## The journal was cleared ```text verify: the journal was cleared: the requests the check counts are gone ``` `ErrCleared`. A clear, such as `POST /reset` or `m.Journal().Clear()`, removed requests the check counts: after the check's mark, while it read the journal, or during its wait. A mark past the journal's newest request, as of a journal that started over, fails this way too. A check without a mark, after a clear, counts what came since: a test that clears the server between its steps checks each step that way. ## A body the journal cut ```text journal: mock: matching: entry 7: journal: the journal kept part of the body, which the query reads ``` `journal.ErrBodyCut`. The journal keeps 1 MiB of each body by default (`journal.max_body_size`). A check that reads the body, with `Body` or a pattern, can't tell whether a request it cut matched, once the rest of the check matches it: what was cut may hold what the matcher looks for. Raise `journal.max_body_size`, or `0` for whole bodies. The server reads no body of a `GET` or `HEAD`, nor under `no_body_parsing`, nor past `MaxBodyParsingLimit`: those requests have `body_truncated` too. ## A check that can't run | Error | Why | What to do | |---|---|---| | `journal.ErrNoJournal` | The server keeps no journal. | `Setup().Journal("")`. | | `ErrNoMethod` | `Method` on a gRPC server, whose calls have none. | Select by `Path`, the full method. | | `ErrOtherJournal` | `Since` with a mark of another server's journal. | Take the mark on the server the check reads. | | `ErrNoRequestPatterns` | `Matching` on a server that takes no request patterns. | Select by the criteria. | | `ErrInvalidSpec` | A check that contradicts itself, such as `Within` and `During` together, `Within` with a count that passes with no request, or an outcome that is none. | Fix the check, as the message says. | A `nil` matcher, `Method()` with no method, `Outcome()` with no outcome and a negative count fail the check too, with a message that names them. The first misuse of a selection is the one reported. ## A request that isn't in the journal Some requests are never journaled, so `Never` passes for them, and other checks don't count them: - Requests the server answers before its mocks get them, such as CORS preflights, or that a middleware answers. - gRPC calls an interceptor answers itself, or that grpc answers before any interceptor, such as one whose request it can't decode. The [Request journal](https://vlatte.dev/more/journal.md#limits) page lists the journal's limits. --- Source: https://vlatte.dev/webhooks/index.md # Webhooks and events Once a mock has replied, it runs its `on_complete` events: each names a listener of the server, which gets the request, the mock and the event's parameters. The `webhook` package is the listener vlatte ships. In Go, `OnCompleteEvent(cfg)` adds an event to a mock, and `Callback(fn)` runs a function of yours, HTTP and gRPC mocks alike. A webhook is a request a mock sends after it replies, as a payment API calls back to say an order was paid. The `webhook` package sends them, as an `on_complete` listener of the server. ## Registering A mock names its listener, so the server registers one under that name first. The `vlatte` command registers a webhook as `webhook`. In Go: ```go wh, err := webhook.New() m := mockhttp.MustNew(mockhttp.Setup().OnCompleteListener("webhook", wh)) ``` A mock that names a listener the server doesn't have fails to build, and so does a server given two listeners of one name. A webhook sends through `webhook.WithTransport`, or else the server's `Setup().Transport`, or else a transport the server makes for webhooks, whose idle connections `m.Close()` closes; a gRPC server makes one too. A listener of your own gets the same from its event's `Kit()`: the server's `Logger()`, `Transport()`, and `Dir()`, the directory of the mock file, which relative files open from. ```yaml request: method: POST path: /orders response: status: 202 on_complete: - name: webhook parameters: method: POST url_template: '{{ .Data.callback }}/orders/{{ (fromJSON .Request.Body.String).id }}' header: content-type: application/json body: '{"status": "paid"}' delay: 500ms ``` In Go: ```go m.MustMock(mockhttp.Postf("/orders"). OnCompleteEvent(webhook.Options(). URL(callback). Method(http.MethodPost). Header("Content-Type", "application/json"). Body(`{"status": "paid"}`). Delay(500 * time.Millisecond). Build("webhook")). Reply(mockhttp.Accepted())) ``` `URLTemplate`, `HeaderTemplate`, `BodyTemplate` and `BodyFile` set the parameters of the same names. A multipart body whose text is templates is `Multipart(mockhttp.MultipartForm(...).Template())`. ## Parameters | Parameter | What it sets | |---|---| | `url` or `url_template` | Where the request goes: an absolute `http` or `https` URL. One of them is required. | | `method` | The method, in any case. Empty means `GET`. | | `header`, `header_template` | Headers by name, each a string, or a list of strings to send the header once per value, in order. Names ignore case, so a name is set once. | | `body`, `body_base64`, `body_template`, `body_file`, `multipart`, `multipart_template` | The body, one of them at most. A body that isn't UTF-8 text goes in `body_base64`, as standard base64, whose line breaks are ignored; a `Body` set in Go that isn't UTF-8 describes there. Multipart bodies are as in [replies](https://vlatte.dev/http/forms-multipart.md). | | `delay` | How long to wait before sending, once the mock replied. The response waits for it too, unless the webhook runs after the response. | | `timeout` | How long the request may take, from sending it to its answer. Without it, the listener's timeout, or else 10s. | Templates render with the data of the request that completed the mock: see [Templates](https://vlatte.dev/templating/index.md). A mistake, such as a missing URL, two bodies or a template that doesn't compile, fails the mock when it is built. ## When they run Webhooks are sent once the mock has replied, each on its own, and the response ends when they all have: a slow or failing webhook holds up no other, and a test checks what they sent right after the call: ```go // callbacks is a mock server standing for the client the webhook calls back. res, err := http.Post(m.URL("/orders"), "application/json", strings.NewReader(`{"id": 42}`)) // ... callbacks.AssertCalled(t) ``` A webhook's `delay` counts from the reply, so it holds the response too. A reply that streams its parts, or a body over a few KB, sends its status and headers before its webhooks are done, so the call returns early; the body still ends after them, unless the mock sets `Content-Length` itself. Read the body to its end, as `io.ReadAll` does, before checking what they sent. `m.Close()` cancels the webhooks still waiting, for their delay or for their target. ## After the response `after_response: true` on an `on_complete` entry, beside `name` and `parameters`, sends the response without waiting for the event, for an app that needs its callback once it has its answer. In Go, set `AfterResponse` on the `vlatte.EventConfig` that `Build` returns. The event starts as the response goes out, so give it a `delay` when the client must read its answer first, and have the test wait for the target to get the webhook, not for the call. `m.Close()` cancels these events too, and waits for them. A failure is logged as any other, but the journal entry of the request, recorded by then, doesn't tell it. ```yaml on_complete: - name: webhook after_response: true parameters: url: http://localhost:8081/orders delay: 100ms ``` ## Failures A webhook fails when its target doesn't answer in time, or answers with a status outside 2xx: a redirect isn't followed. The server logs the failure as a warning, and the request's [journal](https://vlatte.dev/more/journal.md) entry, if the server keeps one, tells it in its `Message`. The timeout is 10 seconds: a webhook's `timeout` parameter (`Options().Timeout(d)` in Go) changes it for that webhook, and `webhook.WithTimeout` for every webhook its listener sends, where 0 means 10 seconds, unlike `proxy.timeout`. The CLI's `webhook.timeout` setting is the `WithTimeout` of its listener, for HTTP and gRPC mocks. A webhook reads up to 256 KiB of the answer, so its connection carries the next one. A response waits for a webhook at most its delay plus the timeout. ## Logs Webhooks log each request and its answer with the server's logger, so `Setup().Silent()` silences them too. `webhook.WithLogger` gives them a logger of their own. ## In gRPC gRPC mocks run `on_complete` events the same way, with `OnCompleteEvent` in Go and `on_complete` in a [gRPC mock file](https://vlatte.dev/grpc/mock-files.md). Register the listener on the gRPC server: ```go wh, err := webhook.New() m := mockgrpc.MustNew(mockgrpc.Setup().OnCompleteListener("webhook", wh)) ``` The webhook's templates render with the data of the call: see [Templates in gRPC](https://vlatte.dev/templating/index.md#in-grpc). --- Source: https://vlatte.dev/tls/index.md # TLS A mock server serves HTTPS, or gRPC over TLS, with one call and no certificate to make. Bring your own certificate, or require client certificates, with one option more. **HTTP** ```go m := mockhttp.MustNew() m.MustStartTLS() // m.URL() starts with https:// t.Cleanup(m.Close) ``` **gRPC** ```go m := mockgrpc.MustNew() m.MustStartTLS() t.Cleanup(m.Close) conn := m.MustDial() // trusts the server's certificate ``` **CLI** ```sh vlatte --https # HTTPS mocks vlatte --grpc-tls ... # gRPC mocks over TLS ``` **Docker** ```sh docker run --rm -p 8443:8443 -e VLATTE_HTTPS=true ghcr.io/caffeine-projects/vlatte:v1.0.0 ``` ## The default certificate Without a certificate of yours, each protocol serves one of its own: | Protocol | Certificate | For clients | |---|---|---| | HTTP | The test certificate of `net/http/httptest`, for `127.0.0.1`, `::1` and `example.com`. | Skip verifying it, as `curl -k` and `InsecureSkipVerify` do, or give the server a certificate you trust. `m.URL()` names `127.0.0.1`, which the certificate covers. | | gRPC | A self-signed certificate made at start, for `localhost`, `127.0.0.1` and `::1`. | `m.Dial()` trusts it. `m.Certificate()` returns it for other clients, to add to a `x509.CertPool`. | ```go // HTTP: a client for the default certificate. client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} // gRPC: a client of your own that trusts the server's certificate. pool := x509.NewCertPool() pool.AddCert(m.Certificate()) conn, err := grpc.NewClient(m.Addr(), grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(pool, ""))) ``` ## Your own certificate The options are the same for HTTP and gRPC, on `Setup()`: | Option | Sets | |---|---| | `TLSCertKeyPair(certFile, keyFile)` | The certificate, from PEM files. | | `TLSCertificates(certs)` | The certificates, as `tls.Certificate` values. | | `TLSMutual(certFile, keyFile, clientCAFile)` | The certificate, and mutual TLS: clients present a certificate the CAs of `clientCAFile` signed. | | `TLSClientCAs(pool)` | Mutual TLS, with the CAs as a `x509.CertPool`. | | `TLSConfig(c)` | The whole TLS configuration. The other TLS options are then ignored. | ```go m := mockhttp.MustNew(mockhttp.Setup().TLSCertKeyPair("testdata/localhost.pem", "testdata/localhost-key.pem")) m.MustStartTLS() ``` Tools such as [mkcert](https://github.com/FiloSottile/mkcert) make a local certificate your machine already trusts: `mkcert localhost 127.0.0.1`. ## Mutual TLS With `TLSMutual` or `TLSClientCAs`, the server asks every client for a certificate, and refuses the handshake of a client without one its CAs signed: ```go m := mockhttp.MustNew(mockhttp.Setup(). TLSMutual("testdata/server.pem", "testdata/server-key.pem", "testdata/client-ca.pem")) m.MustStartTLS() ``` A gRPC client of such a server dials with credentials of its own: `m.Dial(grpc.WithTransportCredentials(...))`. The [HTTPS and mTLS mocks](https://vlatte.dev/guides/https-mtls.md) guide tests a client both ways. ## The CLI and Docker | Setting | Flag | Does | |---|---|---| | `https` | `--https` | Serves the HTTP mocks over HTTPS, with the default certificate unless `tls.cert` is set. | | `tls.cert`, `tls.key` | `--tls-cert`, `--tls-key` | The certificate, from PEM files. | | `tls.ca` | `--tls-ca` | Mutual TLS: the CAs that sign client certificates. | | `grpc.tls` | `--grpc-tls` | Serves the gRPC mocks over TLS, with a self-signed certificate unless `grpc.tls.cert` is set. | | `grpc.tls.cert`, `grpc.tls.key`, `grpc.tls.ca` | `--grpc-tls-cert`, `--grpc-tls-key`, `--grpc-tls-ca` | The same, for gRPC. | ```sh vlatte --https --tls-cert=certs/localhost.pem --tls-key=certs/localhost-key.pem ``` The [Docker](https://vlatte.dev/run/docker.md) image serves HTTPS on port `8443`, in place of `8080`, with `--https` or `VLATTE_HTTPS=true`. Mount the certificate files and name them with `VLATTE_TLS_CERT` and `VLATTE_TLS_KEY`. ## The admin API The admin API serves HTTPS of its own, apart from the mocks: `admin.TLS(config)` in Go, and `admin.tls.cert` with `admin.tls.key` (`--admin-tls-cert`, `--admin-tls-key`) for the command, whose `vlatte health` then asks over HTTPS and trusts that certificate alone. Credentials sent to it travel in plain text without TLS: see [Authentication](https://vlatte.dev/admin-api/index.md#authentication). ## Proxies and recordings When a server sends requests on, how it treats the certificates of the services it reaches depends on what sends them: - [Proxy replies](https://vlatte.dev/http/proxying.md) and the forward proxy don't verify certificates by default, so a target with a self-signed one answers. `SSLVerify(true)`, or `ssl_verify: true`, turns verification on: set it for a target that gets credentials. - A [recording](https://vlatte.dev/recording/settings.md) verifies its target's certificate, as it sends the target credentials. `record.skip_ssl_verify` skips it. - The forward proxy's [HTTPS interception](https://vlatte.dev/http/proxying.md#https-interception) answers HTTPS a client sends through it with certificates its own CA signs, so mocks can answer it. - `Setup().Transport(rt)` sends all of them through a transport of yours, such as one with a client certificate. --- Source: https://vlatte.dev/recipes/index.md # Recipes A recipe prepares a server, for example by adding a set of mocks. `m.Prep(ctx, recipes...)` runs recipes in order, and stops at the first that fails. Recipes live under `recipe//`. ```go m := mockhttp.MustNew() err := m.Prep(ctx, proxyall.New(mockhttp.From("https://api.example.com").Timeout(5*time.Second))) ``` | Recipe | Prepares | |---|---| | [Proxy all](https://vlatte.dev/recipes/proxy-all.md) | A mock that sends what no other mock matches to a real service. | | [GitHub Auth](https://vlatte.dev/recipes/github-auth.md) | "Sign in with GitHub": the OAuth web flow and the user's profile. | | [OpenID Connect](https://vlatte.dev/recipes/oidc.md) | An OpenID Connect provider: discovery, JWKS, signed ID tokens, PKCE and userinfo. | A recipe of your own takes a few lines: see [Writing a recipe](https://vlatte.dev/recipes/writing.md). --- Source: https://vlatte.dev/recipes/proxy-all.md # Proxy all `recipe/http/proxyall` prepares an HTTP server to send what no other mock matches to a real service. Mock the endpoints a test cares about, and let everything else reach the service as it is. ```go import "github.com/caffeine-projects/vlatte/recipe/http/proxyall" m := mockhttp.MustNew() err := m.Prep(ctx, proxyall.New(mockhttp.From("https://api.example.com").Timeout(5*time.Second))) ``` `proxyall.New(reply)` adds the mock `ProxyAll(reply)` makes: it matches any method and path, at priority `math.MaxInt`, so every other mock answers first. It takes the options of the [proxy reply](https://vlatte.dev/http/proxying.md) it is given, such as headers to add or a timeout. Without Go, a mock file's `proxy_all` key, or the `proxy_all` setting (`vlatte --proxy-all-target=https://api.example.com`), does the same: see [Proxy all](https://vlatte.dev/http/proxying.md#proxy-all). --- Source: https://vlatte.dev/recipes/github-auth.md # GitHub Auth `recipe/http/githubauth` prepares an HTTP server to stand in for "Sign in with GitHub": the OAuth web flow of an OAuth app, and the profile of the user who signs in. Point the github.com and api.github.com base URLs of the app under test at the server. ```go import "github.com/caffeine-projects/vlatte/recipe/http/githubauth" m := mockhttp.MustNew() err := m.Prep(ctx, githubauth.New( githubauth.ClientID("my-app"), githubauth.ClientSecret("my-secret"), githubauth.Token("gho_test"), )) ``` It covers the OAuth web flow only: not the device flow, nor GitHub Apps. | Request | Answer | |---|---| | `GET /login/oauth/authorize` with the client ID and a `redirect_uri` | 302 to `redirect_uri` with `code` and the request's `state`, as GitHub does once the user consents. Unlike GitHub, `redirect_uri` is required. | | `POST /login/oauth/access_token` with `Accept: application/json`, and the client ID, the secret and the code in the form | 200 with `access_token`, `token_type` and `scope` as JSON. Without that `Accept`, GitHub answers a form-encoded body: no mock matches. | | The same with a wrong code | 200 with `error: bad_verification_code`, as GitHub answers. | | The same with a wrong client ID or secret | 200 with `error: incorrect_client_credentials`. | | `GET /user` and `GET /user/emails` with `Authorization: Bearer ` or `token ` | The profile and the addresses. | | A `GET` of them without the token | 401, `Bad credentials`. | The code can be exchanged any number of times, unlike GitHub's. | Option | Sets | Default | |---|---|---| | `ClientID(id)`, `ClientSecret(secret)` | The OAuth app's credentials. | `githubauth.DefaultClientID`, `DefaultClientSecret` | | `Code(code)` | The code the authorize endpoint gives. | `DefaultCode` | | `Token(token)`, `Scope(scope)` | The token issued, and the scope it says it has. | `DefaultToken`, `DefaultScope` | | `WithUser(githubauth.User{...})` | The profile of `GET /user`. | octocat | | `Emails(githubauth.Email{...})` | The addresses of `GET /user/emails`. | The profile's email, primary and verified, or none | Mock the rest of the API the app calls, such as `GET /user/repos`, as any other mock: the recipe's 401 answers `/user` and `/user/emails` only. --- Source: https://vlatte.dev/recipes/oidc.md # OpenID Connect `recipe/http/oidc` prepares an HTTP server to stand in for an OpenID Connect provider, for "Sign in with ..." through the authorization code flow: discovery, a JWKS, ID tokens the server signs, PKCE and userinfo. Point the issuer URL of the app under test at the server. The server signs ID tokens with a key of its own, which the recipe can't add: give it one first, with the setting [`template.keys`](https://vlatte.dev/templating/index.md#tokens). ```go import "github.com/caffeine-projects/vlatte/recipe/http/oidc" m := mockhttp.MustNew(mockhttp.Setup().TemplateKeys(oidc.DefaultKeyID + "=RS256")) err := m.Prep(ctx, oidc.New( oidc.ClientID("my-app"), oidc.ClientSecret("my-secret"), oidc.Claims(map[string]any{"sub": "ana", "email": "ana@example.com", "email_verified": true}), )) ``` A server without the key fails `Prep` with an error naming it. In Go, a key of your own goes on the engine with `vlatte.WithSigningKey`; name its kid with `oidc.KeyID(kid)` when it isn't `oidc.DefaultKeyID`. | Request | Answer | |---|---| | `GET /.well-known/openid-configuration` | The issuer, the endpoints below on it, and what the provider supports: the code flow, `client_secret_post`, PKCE S256 and the key's algorithm. | | `GET /jwks` | The public half of the signing key. | | `GET /authorize` with `response_type=code`, the client ID, a `redirect_uri` and a `scope` with `openid` | 302 to `redirect_uri` with a new `code` and the request's `state`. The `nonce`, `code_challenge` and `redirect_uri` are kept with the code. | | The same with a `code_challenge` but no `code_challenge_method=S256` | 302 to `redirect_uri` with `error=invalid_request` and the request's `state`. | | `POST /token` with `grant_type=authorization_code`, the client ID and secret, a `redirect_uri` and a code it issued | `access_token`, `token_type`, `expires_in` and an `id_token` signed with the server's key: the user's claims, `iss`, `aud`, `iat`, `exp`, and the `nonce` when the authorize request sent one. | | The same with a code already exchanged, unknown, issued for another `redirect_uri`, or whose `code_verifier` doesn't fit its `code_challenge` | 400, `invalid_grant`. A code issued with a challenge needs its verifier. | | `GET /userinfo` with `Authorization: Bearer ` | The user's claims. | | Any other `GET /userinfo` | 401 with `WWW-Authenticate: Bearer error="invalid_token"`. | Each code is exchanged once, and PKCE takes S256 only. Every mock is a template, which the admin API shows: codes, PKCE, the `redirect_uri` and single use are a [keyed scenario](https://vlatte.dev/scenarios/index.md#keys) named `oidc-code`, and tokens come from the `jwt` and `jwks` [template functions](https://vlatte.dev/templating/index.md). | Option | Sets | Default | |---|---|---| | `ClientID(id)`, `ClientSecret(secret)` | The app's credentials. ID tokens have the client ID as `aud`. | `oidc.DefaultClientID`, `DefaultClientSecret` | | `KeyID(kid)` | The server's key ID tokens are signed with. Discovery announces its algorithm. | `DefaultKeyID` | | `Issuer(url)` | The `iss` of ID tokens, and the base URL of the endpoints. | The server's URL | | `Claims(map)` | The user's claims, in ID tokens and from userinfo. | `oidc.DefaultClaims()` | | `AccessToken(token)`, `TokenTTL(d)` | The access token issued, and how long tokens last. | `DefaultAccessToken`, `DefaultTokenTTL` (1h) | | `WithPaths(oidc.Paths{...})` | Where the endpoints are, such as Google's `/o/oauth2/v2/auth`. | `oidc.DefaultPaths()` | It covers the authorization code flow only: not the implicit or hybrid flows, refresh tokens, nor `client_secret_basic`. --- Source: https://vlatte.dev/recipes/writing.md # Writing a recipe A recipe is a `vlatte.Recipe`: its `Prep(ctx, input)` gets the server in `input.Server`, and adds mocks or changes what it serves. `mockhttp.Recipe` and `mockgrpc.Recipe` name the recipes of each protocol. ```go type users struct{} func (users) Prep(_ context.Context, in *vlatte.RecipeInput[*mockhttp.Mock, *mockhttp.Server]) error { _, err := in.Server.Mock( mockhttp.Getf("/users").Reply(mockhttp.OK().JSON([]any{})), mockhttp.Postf("/users").Reply(mockhttp.Created()), ) return err } ``` Run it as you run the built-in ones: `m.Prep(ctx, users{})`. --- Source: https://vlatte.dev/more/mock-lifecycle.md # Mock lifecycle These features work the same on HTTP and gRPC mocks, in Go and in mock files. | Go | Mock file | What it does | |---|---|---| | `ID(id)` | `id` | Names the mock in the admin API, as in `/mocks/{id}`, the logs and the debug headers. Without one, the server gives it an ID. | | `Name(name)` | `name` | A readable name, in the logs, the near misses and the debug headers. | | `Priority(p)` | `priority` | Mocks with a lower priority are matched first; mocks of equal priority, in the order they were added. | | `Enabled(false)` | `enabled: false` | Starts the mock disabled. | | `Times(n)`, `Once()` | `repeat: n` | Serves `n` requests, then stops matching. | | `Delay(d)` | `delay` | Waits `d` before replying. | | `DelayFunc(fn)` | | Picks the delay of each reply, such as `vlatte.Jitter(avg, deviation)`, where deviation is a percent of avg from 0 to 200, `vlatte.Latency(d)` or `vlatte.UniformDistribution(min, max)`. | ```go m.MustMock(mockhttp.Getf("/users"). ID("users-list").Name("list users"). Priority(1).Times(3). DelayFunc(vlatte.Jitter(200*time.Millisecond, 20)). Reply(mockhttp.OK().JSON([]any{}))) ``` ```yaml id: users-list name: list users priority: 1 repeat: 3 delay: 200ms request: method: GET path: /users response: status: 200 body: [] ``` ## Times `Times` counts per mock: a mock built again, as `Remock` does, starts from zero. A mock used up by its `Times` no longer matches, and a request it would have answered shows it first in the [near misses](https://vlatte.dev/http/troubleshooting.md#near-misses), as differing in `times` only. A gRPC answer with a status counts as the mock's answer, for `Times` too. ## Delays A mock's delay comes before its reply: a client that leaves during it gets nothing, and a [proxy reply](https://vlatte.dev/http/proxying.md)'s target isn't called. Durations in mock files are whole milliseconds (`1500`) or a Go duration (`1.5s`). For delays inside an answer, such as between the parts of a body, see [Pipes](https://vlatte.dev/http/pipes.md) and [Faults](https://vlatte.dev/http/faults.md) for HTTP, and `MessageDelay` for [gRPC streams](https://vlatte.dev/grpc/streaming.md). ## Enabling and disabling A scope's `Enable()` and `Disable()` turn its mocks on and off, and the server's turn every mock. The [admin API](https://vlatte.dev/admin-api/index.md) does it with `PATCH /mocks/{id}` and `{"enabled": false}`. ## Priorities and proxies A mock that proxies every request needs a priority number above 0 to let other mocks answer first: `AnyMethod().Priority(10).Reply(From(target))`. `ProxyAll` mocks come last, at priority `math.MaxInt`. --- Source: https://vlatte.dev/more/journal.md # Request journal A server can keep a journal of the requests its mocks get, whatever their outcome: a mock answered it (`matched`), none matched (`unmatched`), another server did (`proxied`), serving it failed (`failed`), or its client left first (`abandoned`). A test then [verifies the requests](https://vlatte.dev/verify/index.md) it sent, while matching and hits work as before. Each entry has the status the client got: the one a reply that writes its own answer sent, as `Handler` does, `200` when it sent none, and for a failure the status sent before it, else `error_status_code`, as for a panic. A middleware (`Use`) or the decorator (`HandlerDecorator`) that answers before the mock, in its place, or once a timeout ends it, sets that status, as it does in the logs and the admin API's events. ```go m := mockhttp.MustNew(mockhttp.Setup().Journal("")) // "" keeps it in a temporary directory, which Close removes m.MustStart() t.Cleanup(m.Close) // ... the code under test sends its requests ... // journal is github.com/caffeine-projects/vlatte/journal. m.VerifyRequests().Method("POST").Path(match.Equal("/orders")).Times(t, 1) m.VerifyRequests().Header("Authorization", match.HasPrefix("Bearer ")).AtLeast(t, 2) m.VerifyRequests().Outcome(journal.OutcomeUnmatched).Never(t) // The request part of a mock selects as a mock matches: query parameters, forms, files, the parsed body. m.VerifyRequests().Matching(mockhttp.Request().Query("page", match.Equal("2"))).Times(t, 1) // The journal itself pages through the requests, newest first. page, err := m.Journal().Find(ctx, journal.Query{Path: match.GlobMatch("/users/*"), Limit: 10}) ``` [Verifying requests](https://vlatte.dev/verify/index.md) covers the checks: their criteria, failure reports, waits, marks and order, and the admin API's `QUERY /requests/verify`. An entry's `Message` tells what failed: the request, or else the on_complete events of a request a mock answered, one line each, such as a [webhook](https://vlatte.dev/webhooks/index.md) whose target answered `500`, so a test checks a webhook failed without reading the logs. A request is journaled as the server ends its answer, as hits count: a client gets a small answer only then, while it may read the start of a large one a moment before, so a check of a request sent in the background [waits](https://vlatte.dev/verify/waiting.md). `m.Journal()` is the journal itself. A `journal.Query`, of package `github.com/caffeine-projects/vlatte/journal`, selects by the fields every protocol has, as the criteria of a check do. `Kind`, `Outcome`, `MockID`, `Method`, `Path` and `Status` are indexed: a query runs each of their matchers once per value the journal holds, not once per request, and reads no request from disk for them. `Target` (the full URL), `Since` and `Until` filter in memory too. `Header`, `Body` and request patterns read the requests the other fields leave. `Limit` and `Before` page through them, newest first, and `After` leaves out those up to a mark; `m.Journal().Find` returns the next page's cursor and the count. A `journal.Journal` works on its own too, as a changelog of any requests: `journal.New(journal.Options{Dir: dir})`. Patterns are the request part of a mock builder, such as `mockhttp.Request()`, which `Matching` takes: the server rebuilds each request from the journal and matches it, through its body parsers. A pattern has no `Times` or `Once`, which count served requests, nor scenario, which moves when its mock is picked, rather than match one, and no `ClientIP`: a rebuilt request has no `RemoteAddr`, nor TLS state for a `RequestMatches` predicate. ## Files The journal is a changelog: each request is a JSON line, appended to `journal-NNNNNN.log` files. A new file starts every quarter of `journal.max_entries` requests (10 000 at most) or 64 MiB, so dropping the oldest requests removes whole files. Each full file, and the last one on a clean stop, gets a `.idx` sidecar with its index, so a restart reads no log. A text body stays text; another is `body_base64`. `DELETE /requests` and `Clear` write a `journal.clear` tombstone before they delete the files, so a restart drops a file a crash or a failed delete left. Only the user reads the files. ## Persistence A named directory keeps the requests across restarts: a server that starts again finds the requests it served before, and a check without a mark counts them too, so tests use `Journal("")`, and admin API clients a mark or a reset first. One server at a time keeps a journal in a directory: another fails to start. An HTTP server's relative directory starts at `root_dir`, and `{workdir}` at its [work directory](https://vlatte.dev/run/configuration.md#work-directory), as in `{workdir}/_journal`. Close closes the journal: verify the requests before it, as `t.Cleanup(m.Close)` leaves a test to. ## Limits - The journal keeps the newest 10 000 requests (`journal.max_entries`, `0` for all), and 1 MiB of each body (`journal.max_body_size`, `0` for whole bodies). - A body the journal cut, or that the server didn't read, has `body_truncated`: the server reads no body of a `GET` or `HEAD`, nor under `no_body_parsing`, nor past `MaxBodyParsingLimit`. A request pattern that reads the body, such as `Body`, `FormFile` or `RequestMatches`, which gets the whole request, fails on such a request, when the rest of the pattern matches it, rather than miss it: `Never` can't pass for a request the server got. - The headers the server redacts in its logs (`RedactHeader`) are redacted in the journal. gRPC metadata is kept as it came. - Requests the server answers before its mocks get them, such as CORS preflights, or that a middleware answers, aren't journaled: `Never` passes for them. - A named directory gets a `.gitignore` of its files, so committing `testdata` leaves the journal out. The settings `journal` (`--journal`, `VLATTE_JOURNAL`), `journal.dir`, `journal.max_entries` and `journal.max_body_size` configure it. Through the [admin API](https://vlatte.dev/admin-api/index.md), `QUERY /requests` takes a query in the body, with the matchers of mock files, and answers the requests, newest first, their `count`, and `mark`, the number of the newest request of the journal. `"limit": 0` answers the count alone. [`QUERY /requests/verify`](https://vlatte.dev/verify/admin-api.md) checks them, as a Go test does. `request` is the request part of a mock file: ```sh curl -X QUERY -H 'Content-Type: application/json' \ -d '{"method": "POST", "path": "/orders", "request": {"query": {"page": "2"}}, "limit": 0}' \ http://127.0.0.1:8081/requests # {"count": 1, "requests": [], "mark": 41} ``` | Key | Selects | |---|---| | `kind`, `outcome` | The protocol, such as `http`, and the outcome, exactly. | | `mock_id`, `method`, `path`, `target`, `status`, `body` | By a matcher: text matches equal text, a number or a bool an equal value, and a list is a matcher, such as `["glob", "/users/*"]` or `["jsonpath", "$.id", ["equal", 7]]`. | | `header` | Headers, or metadata, by name in any case, each by a matcher. | | `since`, `until` | The time of the requests, in RFC 3339. | | `request` | The request part of an HTTP or gRPC mock file. Send `path` on its own too, and `method` for HTTP: the index narrows the requests before `request` reads them. | | `limit`, `before` | The page: 100 requests by default, 1000 at most, and the `next` of the page before. | | `after` | The requests after a `mark`. | ## In gRPC gRPC servers keep the same journal with `mockgrpc.Setup().Journal(dir)`, and [verify](https://vlatte.dev/verify/selecting.md#grpc-calls) it the same way: a call's path is its full method, such as `/pkg.Service/Method`, its headers its metadata, its body its request message as JSON, which `match.Field` and `match.JSONPath` read, and its status its code, the one the client got, after every interceptor of `ServerOptions`. Its answer's `vlatte-request-id` header names its entry. A client or bidi stream's body is the JSON array of the messages its client sent. Patterns are gRPC mock builders, such as `mockgrpc.Method("Greetings").Field("message", match.Equal("hi"))`, and the admin API's `request` key takes the request part of a gRPC mock file. For example, query the gRPC server's admin API for calls with a particular request field. Set `GRPC_ADMIN_URL` to the address the command prints for that admin API, or `m.AdminURL()` in Go: ```sh curl -X QUERY -H 'Content-Type: application/json' \ -d '{"path":"/helloworld.Greeter/SayHello","request":{"method":"helloworld.Greeter/SayHello","fields":{"name":"ann"}}}' \ "$GRPC_ADMIN_URL/requests" ``` `request.method` is required and names the gRPC method, as in a mock file. The top-level `path` selects its full method through the journal's index. A gRPC call has no HTTP method: don't send a top-level `method` criterion. - With a journal, a stream interceptor goes through `grpc.ChainStreamInterceptor`: the journal takes `grpc.StreamInterceptor`, which grpc runs before every other, and `New` fails with `mockgrpc.ErrStreamInterceptorSlot` when `ServerOptions` sets it. A unary interceptor goes either way. - A call an interceptor answers itself isn't journaled, nor one grpc answers before any interceptor, such as one whose request it can't decode. A call of a method no descriptor describes is journaled as unmatched. - A unary reply grpc fails to send once every interceptor returned, such as one larger than a `grpc.MaxSendMsgSize` set, keeps the code they returned. - A service registered on `GRPCServer()`, past the server, has its unary calls journaled within the interceptors, with the code its mocks answered with, which `Start` warns of: register services with `RegisterService`, or `pb.RegisterGreeterServer(m, ...)`. --- Source: https://vlatte.dev/admin-api/index.md # Admin API The admin API manages a server while it runs. It is an HTTP server of its own, on its own address, the same for mock servers of every protocol, and it leaves every path of the mock server to the mocks. It serves only when asked, from the `admin` package: ```go m := mockhttp.MustNew(mockhttp.Setup().Admin(admin.MustNew(admin.Addr("127.0.0.1:8081")))) // no Addr picks a random local port m.MustStart() fmt.Println(m.AdminURL()) // http://127.0.0.1:8081 ``` A gRPC server serves the same admin API with `mockgrpc.Setup().Admin(admin.MustNew(...))`, with gRPC mock files, and recordings with the keys of its `grpc.record` section. An `Admin` serves one mock server. A server that doesn't ask for an admin API links neither it nor what it serves with. `admin.Use(middlewares...)` wraps every HTTP request of the admin API, `POST /mcp` included, in the middlewares given, the first outermost, before the credentials are checked, so one can answer a CORS preflight, which carries none. MCP over stdio doesn't go through them. ## Authentication `admin.BasicAuth(username, password)` and `admin.BearerToken(token)` ask every HTTP request of the admin API for credentials: the pages, `/openapi`, the streams, `POST /mcp` and unknown paths too. With both, a request passes with either. A request without them gets `401` with the code `unauthorized`, and `WWW-Authenticate` names the schemes taken. Only the Kubernetes probes, `/livez`, `/readyz` and `/startupz`, need none, as a probe that needs a secret turns a wrong one into a restart loop: they answer their status alone then, and a `503` that doesn't say why, which the server logs. `/health`, which says what the server serves, needs them. ```go api := admin.MustNew( admin.Addr(":8081"), admin.BasicAuth("ops", os.Getenv("ADMIN_PASSWORD")), admin.BearerToken(os.Getenv("ADMIN_TOKEN")), admin.TLS(&tls.Config{Certificates: []tls.Certificate{cert}}), ) ``` A browser shows its sign-in prompt for the admin pages, and sends what it was given with each of their requests and live streams. It sends no bearer token, so the prompt takes the token as the password, with any username. Basic has no sign-out: closing the browser forgets the credentials. A token goes in `Authorization: Bearer`, never in a query parameter. Credentials are compared in constant time, and `New` refuses empty ones, a username with `:`, control characters, and a token an `Authorization` header can't carry; its errors never quote them. Generate a token with `openssl rand -hex 32`. Basic and bearer credentials travel in plain text over HTTP: `admin.TLS(config)` serves HTTPS, and `AdminURL` starts with `https://`. The admin API logs a warning when it listens beyond the local host without credentials, or with credentials over plain HTTP. ## In the vlatte command The `vlatte` command serves the admin API by default: `admin: false` (`--admin=false`, `VLATTE_ADMIN=false`) turns it off, and `admin.addr` (`--admin-addr`, `VLATTE_ADMIN_ADDR`) sets its address. Its gRPC server serves an admin API of its own, which `grpc.admin: false` (`--grpc-admin=false`) turns off and `grpc.admin.addr` (`--grpc-admin-addr`) places. These settings are the command's: a server made in Go ignores them, even in a `.vlatterc` it shares with the command, and serves an admin API only when its code asks for one. The command's credentials and certificate apply to both admin APIs. `admin.auth.username` with `admin.auth.password` asks for Basic, and `admin.auth.token` for a bearer token. The password and the token have no flag, as the command line shows to every user of the host: set them in the environment (`VLATTE_ADMIN_AUTH_PASSWORD`, `VLATTE_ADMIN_AUTH_TOKEN`), in a config file, or in a file that `admin.auth.password_file` or `admin.auth.token_file` names, such as a Docker or Kubernetes secret, whose trailing line break isn't part of it. `admin.tls.cert` and `admin.tls.key` (`--admin-tls-cert`, `--admin-tls-key`) serve HTTPS. `vlatte health` reads the same settings: with a certificate, it asks the admin API over HTTPS, and trusts that certificate alone. ```sh docker run --rm -p 8080:8080 -p 8081:8081 \ -v "$PWD/secrets:/run/secrets:ro" -v "$PWD/certs:/certs:ro" \ -e VLATTE_ADMIN_AUTH_TOKEN_FILE=/run/secrets/admin_token \ -e VLATTE_ADMIN_TLS_CERT=/certs/admin.pem -e VLATTE_ADMIN_TLS_KEY=/certs/admin-key.pem \ ghcr.io/caffeine-projects/vlatte:v1.0.0 ``` See [Docker](https://vlatte.dev/run/docker.md) for the image's ports and settings. ## Routes The [OpenAPI reference](https://vlatte.dev/reference/admin-api.html){target="_blank" rel="noopener noreferrer"} describes every route with its parameters, bodies and answers. Its document is at [`https://vlatte.dev/openapi/admin.yaml`](https://vlatte.dev/openapi/admin.yaml){target="_self"}, for client generators and API tools, and a running server serves it at `GET /openapi`. | Route | What it does | |---|---| | `GET /health` | Answers once the admin API serves, with the kind of the server and the URL of its mocks. | | `GET /livez` | The Kubernetes liveness probe: answers as `/health` does, whatever the mock store says. Needs no credentials, and answers its status alone when the admin API asks for some. | | `GET /readyz` | The Kubernetes readiness probe: answers as `/health` does once the mock store's `Health` returns nil, and `503` while it returns an error. Needs no credentials, as `/livez`. | | `GET /startupz` | The Kubernetes startup probe: answers as `/livez` does. The admin API serves only once the mock server loaded its mocks. | | `POST /reset` | Removes the mocks added through the admin API, loads the mock files again, saved ones included, starts every scenario over, and clears the [journal](https://vlatte.dev/more/journal.md). Mocks added in code, and the [key-value store](https://vlatte.dev/templating/index.md#key-value-store), stay as they are. The mocks of a persistent store come back from it: reset never deletes what it holds. | | `GET /mocks` | Lists every mock, enabled or not, with its `priority`, `enabled`, `hits` and `origin`. | | `POST /mocks` | Adds the mock of the body, a [mock file](https://vlatte.dev/http/mock-files.md). An id in use is a `409`. | | `GET /mocks/{id}` | The mock as a mock file describes it, which `PUT` and `POST` take back. A mock whose reply a mock file can't hold, an `Echo`, `Func` or `Handler`, answers `500` with the code `describe_failed`. A gRPC mock built in Go does too when any part of it is one a mock file can't hold, such as a `Shared` reply, a `CallMatches` matcher, a callback or a `DelayFunc` delay, and the detail names it. | | `PUT /mocks/{id}` | Replaces the mock, in one step. The body's id must be the path's. | | `PATCH /mocks/{id}` | Enables or disables the mock: `{"enabled": false}`. | | `DELETE /mocks/{id}`, `DELETE /mocks` | Removes the mock, or every mock. | | `POST /mocks/validate` | Checks a mock as `POST /mocks` would, without adding it. | | `GET /schema` | The [JSON Schema](https://vlatte.dev/reference/schemas.md) of mock files, for clients that write them. A gRPC server answers `501`. | | `POST /mocks/reload` | Loads the mock files, and the [persistent store](https://vlatte.dev/admin-api/index.md#persistent-admin-mocks), again, as `m.Reload()` does. A mock file's mock takes the place of a mock added through the admin API with its id. | | `POST /mocks/save` | Writes the mocks added through the admin API as mock files, `.mock.`, and serves them from the files from then on. `{"dir": ..., "format": ...}` defaults to `{workdir}/_mocks` and YAML, which the default mock file patterns load on the next start; `dir` stays inside `root_dir`. When a mock can't be written as a mock file that loads back, nothing is written, and files that fail to load are put back as they were. | | `GET /scenarios`, `PUT /scenarios/{name}`, `DELETE /scenarios/{name}`, `DELETE /scenarios` | Lists the state of each scenario, moves one to a state, `{"state": "paid"}`, starts one over, and starts every scenario over. Names ignore case: `Checkout` is `checkout`. `?key=t1` moves or starts over the instance of a key only. | | `GET /kv`, `GET /kv/{key}`, `PUT /kv/{key}`, `DELETE /kv/{key}` | Lists the keys of the [key-value store](https://vlatte.dev/templating/index.md#key-value-store) templates read, in order, gets the value of a key, sets one, `{"value": "abc123"}`, and removes one. Templates read a change from the next request on. A key is one path segment, so a key with a slash is sent escaped: `/kv/a%2Fb` is `a/b`. Removing a key that isn't set answers `204` too. | | `POST /kv/reload` | Loads the key-value store again from its CSV file, in place of every value: values set since are gone, and keys removed since come back. A file with a problem changes nothing, and the answer names its line. A store with no file answers `501`. `POST /mocks/reload` and `POST /reset` leave the store as it is. | | `GET /requests` | Lists the newest requests of the [journal](https://vlatte.dev/more/journal.md), with their `count` and the journal's `mark`: `?limit=` (100 by default, 1000 at most, `0` for the count alone) and `?before=`, the `next` of a page, page through them, and `?after=`, a mark, leaves out the requests up to it. | | `QUERY /requests` | Finds and counts the requests a query selects, in the body: see [Request journal](https://vlatte.dev/more/journal.md). It uses the HTTP [`QUERY`](https://httpwg.org/specs/rfc10008.html) method, safe as `GET` is, with a body: `curl -X QUERY`. | | `QUERY /requests/verify` | Checks the requests a query selects, their count, over a wait or not, or their order, and answers the verdict: see [Verifying requests](https://vlatte.dev/verify/admin-api.md). | | `GET /requests/{id}`, `DELETE /requests` | Gets a request by the id its answer's `VLatte-Request-Id` names, and clears the journal. | | `GET /events` | Streams what the server serves as it happens, as [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html): requests a mock answered, requests no mock matched, proxied requests, warnings, errors and [scenario](https://vlatte.dev/scenarios/index.md) moves. Nothing is kept: the [journal](https://vlatte.dev/more/journal.md) keeps the requests. `curl -N /events` follows it. | | `GET /logs` | Streams the server's log lines, as it writes them, as Server-Sent Events. A server given its own `Logger` streams none. | | `POST /recording`, `GET /recording`, `DELETE /recording` | Starts a [recording](https://vlatte.dev/recording/index.md) with the keys of the `record` section, counts its calls, and stops it, listing the files it wrote. | | `GET /` | The admin's homepage: what the mock server serves and what it served, its mocks, requests, scenarios and recording, live, with links to the other pages. Every page has a navbar with a light and dark theme toggle. | | `GET /recorder` | A page that records a service from a browser. | | `GET /scenario-board` | A page that shows and moves [scenarios](https://vlatte.dev/scenarios/index.md) from a browser, live. | | `POST /mcp` | The [MCP](https://vlatte.dev/mcp/setup.md) endpoint, when the admin API serves MCP; `501` otherwise. | | `GET /docs`, `GET /openapi` | A page that renders the OpenAPI document, and the document. The page loads [Scalar](https://scalar.com) from a CDN, so it needs internet: the version vlatte pins, checked against its hash, or a newer one that `admin.DocsVersion("1.80.0")` (`admin.docs_version`, `--admin-docs-version` for the command) names. | ## Formats Bodies are JSON, or YAML with `Content-Type: application/yaml`. Answers follow `Accept`: JSON, or YAML when it is preferred. A relative `body_file` in a mock sent to the admin API opens from the server's working directory, as `GET /mocks/{id}` writes it. ## Errors Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem documents with a stable `code`; the problems of a mock are listed one by one: ```json { "status": 400, "title": "Bad Request", "code": "invalid_body", "detail": "the mock has 1 problem", "instance": "/mocks", "problems": [ { "code": "unknown_key", "origin": "admin request", "key": "response.stauts", "message": "response.stauts is not a known setting", "hint": "did you mean response.status?" } ] } ``` A request net/http refuses before the admin API reads it, such as one with a malformed request line, is answered in plain text. ## Safety Without [credentials](#authentication), anyone who reaches the admin API can change the mocks, which can read files the server can, and read the requests of the journal. It listens on a local address unless told otherwise; with `VLATTE_DOCKER_HOST`, the CLI listens on every interface, as for the mocks, so set credentials and TLS, or publish its port only where the network is trusted. A browser's cross-site request that would change the server, or query its journal, is refused with `403`, credentials or not: a browser sends those of its sign-in prompt on its own. ## Persistent admin mocks The mocks the admin API adds last as long as the server, unless a persistent store keeps them. Each change the admin API makes, adding, replacing, enabling, disabling or removing a mock, reaches the store at once, and the server serves what the store holds when it starts and on each reload, so a change made to the store directly takes effect then too. The mocks of mock files and code never reach it, and a mock file's mock wins over a mock of the store with its id. A reload swaps the mocks of the store and of the mock files in one step: when either fails, the server serves what it served before. `FileStore(dir)` keeps them as mock files, `.mock.yaml`, which only the store loads: keep `dir` out of the mock file patterns. A relative `dir` starts at `root_dir`, and `{workdir}` at the [work directory](https://vlatte.dev/run/configuration.md#work-directory). A mock whose description doesn't load back is refused, as its file would fail every later start. A file's mock keeps the ID its name gives: one whose `id` differs fails the start and the reload, as the store finds its file by that name. The `vlatte` command keeps them in `{workdir}/_admin`: `admin.dir` (`--admin-dir`, `VLATTE_ADMIN_DIR`) sets another directory, and an empty one keeps the mocks in memory. It keeps those of the gRPC server's admin API in `{workdir}/_grpc_admin`, the HTTP server's work directory, as the gRPC server has none: `grpc.admin.dir` (`--grpc-admin-dir`, `VLATTE_GRPC_ADMIN_DIR`) sets another, whose relative paths start at `grpc.root_dir`, and an empty one keeps them in memory. A kept gRPC mock of a method the descriptor sets no longer hold fails the start, which names its file: remove the file to start again. In Go, `AdminStore` needs `Admin`: ```go m := mockhttp.MustNew(mockhttp.Setup().Admin(admin.MustNew()).AdminStore(mockhttp.FileStore("{workdir}/_admin"))) ``` Any `vlatte.Store[*mockhttp.Mock]` can be the store, such as a database collection. It keeps the descriptions of the mocks by ID: `EncodeMock` writes one, and `DecodeMock` builds the mock back for the server. Its `Health(ctx)` returns an error while it can't serve the mocks, such as when the database is down: `GET /readyz` answers `503` until it returns nil. ```go mockhttp.Setup().AdminStore(func(app *mockhttp.Server) (vlatte.Store[*mockhttp.Mock], error) { return &collectionStore{app: app, coll: coll}, nil // Store: EncodeMock(mock, "json"); PickAll: DecodeMock(app, doc, "json") }) ``` A gRPC server takes a store the same way, `mockgrpc.Setup().Admin(admin.MustNew()).AdminStore(mockgrpc.FileStore(dir))`, with `mockgrpc.EncodeMock` and `mockgrpc.DecodeMock` for a store of yours. Its relative `dir` starts at its root directory. `POST /mocks/save` is apart from the store: it exports the admin API's mocks as mock files to commit, which then serve in place of the store's copies. --- Source: https://vlatte.dev/agents/index.md # Coding Agents Use a coding agent to write vlatte tests and mock files, or connect it to a running mock server to inspect and change what it serves. The documentation and the server's MCP tools support different parts of that work. | You want the agent to… | Start with | |---|---| | Learn the API and write tests or mock files | [Documentation for agents](https://vlatte.dev/agents/documentation.md) | | Inspect mocks, add answers and check requests on a running server | [MCP overview](https://vlatte.dev/mcp/index.md) | | Connect through stdio or Streamable HTTP | [MCP setup](https://vlatte.dev/mcp/setup.md) | | Find a server tool and its arguments | [MCP tools](https://vlatte.dev/mcp/tools.md) | ## Give the agent the documentation Start with [`llms.txt`](https://vlatte.dev/llms.txt), the index of documentation pages with links to plain Markdown. Have the agent fetch the pages for its task. [`llms-full.txt`](https://vlatte.dev/llms-full.txt) holds the full reference when it needs broader context. For example: ```text Read https://vlatte.dev/llms.txt and the pages relevant to HTTP mocking. Add a test for this client's retry behavior using vlatte. Follow this project's existing test conventions, create the mock server in the test, and check both the result and the requests the client sent. ``` The [documentation page](https://vlatte.dev/agents/documentation.md) lists the Markdown URLs, schemas and OpenAPI document, with a short instruction you can put in your own project. ## Let the agent use a running server The `vlatte` command serves its [admin API](https://vlatte.dev/admin-api/index.md) operations over MCP. An agent can inspect the mock schema, validate and add mocks, record a service, move scenarios, and verify the requests your application sent. It can save the mocks as files for later runs. For a local agent, configure it to start the command: ```json { "mcpServers": { "vlatte": { "command": "vlatte", "args": ["mcp", "--mock-files=mocks/*.yaml"] } } } ``` Install the [CLI](https://vlatte.dev/getting-started/installation.md#the-cli) first. The paths are relative to the command's working directory: point your agent's configuration at the project that holds the mock files. [MCP setup](https://vlatte.dev/mcp/setup.md) also covers connecting to an existing server over HTTP. Try the [inspect, add and verify workflow](https://vlatte.dev/mcp/index.md#inspect-add-and-verify). MCP operates on the server; the documentation files above teach the agent how to use vlatte in your code. To test an application that calls a remote MCP server, see [Mocking a remote MCP server](https://vlatte.dev/guides/mcp-server.md). That guide uses vlatte to play the upstream server your application calls. --- Source: https://vlatte.dev/agents/documentation.md # Documentation for agents vlatte publishes plain Markdown documentation and machine-readable API schemas. Start with the index, then fetch the pages needed for the task rather than loading the full reference for every change. ## Choose a resource | Resource | Use it for | |---|---| | [`llms.txt`](https://vlatte.dev/llms.txt) | Find pages by topic, with links to their Markdown copies. | | [`llms-full.txt`](https://vlatte.dev/llms-full.txt) | Read the documentation in one file, with each page's source URL. | | Individual Markdown pages, such as [`http/matching.md`](https://vlatte.dev/http/matching.md) | Read a focused reference without the website navigation. | | [HTTP mock file schema](https://vlatte.dev/schemas/http.schema.json) | Complete and check the structure of HTTP mocks. | | [gRPC mock file schema](https://vlatte.dev/schemas/grpc.schema.json) | Complete and check the structure of gRPC mocks. | | [Admin API OpenAPI document](https://vlatte.dev/openapi/admin.yaml) | Find REST operations, parameters and response formats. | | [Go API](https://vlatte.dev/reference/go-api.md) | Read exported package contracts on pkg.go.dev. | ```sh curl -fsSL https://vlatte.dev/llms.txt curl -fsSL https://vlatte.dev/getting-started/quick-start.md curl -fsSL https://vlatte.dev/http/matching.md ``` Follow the Markdown URLs in the index: a section's overview is normally `index.md`, such as `/http/index.md`. Code snippets included from the repository are expanded in the Markdown copies too. The site also publishes [`robots.txt`](https://vlatte.dev/robots.txt) and [`ai.txt`](https://vlatte.dev/ai.txt) for crawler access; use `llms.txt` to find the documentation. ## Give the agent a task | Task | Useful pages | |---|---| | Write a Go HTTP client test | [Quick start](https://vlatte.dev/getting-started/quick-start.md), [REST client guide](https://vlatte.dev/guides/testing-a-rest-client.md), [Verifying requests](https://vlatte.dev/verify/index.md) | | Serve mocks for a browser or mobile app | [Frontend client guide](https://vlatte.dev/guides/testing-a-frontend-client.md), [Mock files and the CLI](https://vlatte.dev/guides/mock-files-and-the-cli.md), [Docker](https://vlatte.dev/run/docker.md) | | Test a gRPC client | [gRPC overview](https://vlatte.dev/grpc/index.md), [Streaming client guide](https://vlatte.dev/guides/grpc-streaming-client.md) | | Capture an upstream and replay it | [Record and replay guide](https://vlatte.dev/guides/record-and-replay.md), [Files and secrets](https://vlatte.dev/recording/files-secrets.md) | | Work with mocks on a running server | [MCP setup](https://vlatte.dev/mcp/setup.md), [MCP tools](https://vlatte.dev/mcp/tools.md) | Ask the agent to inspect the project's dependency version and test conventions before adding code. When using the Go library, the installed version's exported API can be checked with `go doc`; the documentation on this site may describe a newer revision than the version your project uses. ## Project instructions You can add an instruction like this to your own project's `AGENTS.md`, or put it directly in a prompt: ```text For vlatte tests and mock files, start from https://vlatte.dev/llms.txt and read the Markdown pages relevant to the task. Check the installed vlatte version and follow this project's test conventions. Run the tests you change. For mock files, use the published JSON Schema and check that the files load in the vlatte version this project runs. ``` The schemas check file structure. A file can still fail to load if it names a missing body file, descriptor or other runtime resource: see [Mock file schemas](https://vlatte.dev/reference/schemas.md). --- Source: https://vlatte.dev/mcp/index.md # MCP overview vlatte speaks the [Model Context Protocol](https://modelcontextprotocol.io): an AI agent, such as a coding assistant, can drive a mock server with the same operations the [admin API](https://vlatte.dev/admin-api/index.md) serves over REST. It sees every mock, adds and changes them, records a real service, moves scenarios, and reads and checks the requests clients sent. | An agent can… | With the tools | |---|---| | See the mocks the server serves, and the schema of mock files | `list_mocks`, `get_mock`, `mock_schema` | | Write a mock, check it, and serve it at once | `validate_mock`, `add_mock`, `replace_mock`, `update_mock` | | Keep the mocks it wrote as mock files | `save_mocks`, `reload_mocks` | | Record a real service as mock files | `start_recording`, `recording_status`, `stop_recording` | | Walk a stateful flow | `list_scenarios`, `set_scenario`, `reset_scenario` | | Find what a client sent, and check it as a test does | `list_requests`, `query_requests`, `verify_requests` | | Start over | `reset` | Every tool is listed in [MCP tools](https://vlatte.dev/mcp/tools.md). For documentation you can give an agent before it writes code, see [Coding Agents](https://vlatte.dev/agents/index.md). To mock an MCP server that your code calls, rather than drive vlatte from an agent, see [Mocking a remote MCP server](https://vlatte.dev/guides/mcp-server.md). ## Connect an agent The `vlatte` command serves MCP two ways, and so does a server built in Go with `admin.MCP()`: - **stdio**: the agent starts `vlatte mcp` as its MCP server, and talks to it on stdin and stdout. - **Streamable HTTP**: the agent calls `POST /mcp` on the admin API of a server that runs already, in a test, a container or another terminal. ```json {"mcpServers": {"vlatte": {"command": "vlatte", "args": ["mcp", "--mock-files=mocks/*.yaml"]}}} ``` [MCP setup](https://vlatte.dev/mcp/setup.md) shows both, from the CLI and from Go, and how to turn MCP off. ## Inspect, add and verify Once the agent is connected, this workflow adds an HTTP mock, exercises it and checks the request. The CLI keeps a journal by default; a Go server needs `Setup().Journal("")` to offer the request tools. 1. Call `health` to find the mock server's URL, and `list_mocks` to inspect what it already serves. Call `mock_schema` for the HTTP mock file schema. 2. Call `validate_mock` with the arguments below, then `add_mock` with the same arguments once validation succeeds: ```json { "mock": { "id": "agent-hello", "request": {"method": "GET", "path": "/hello"}, "response": {"status": 200, "body": "hello"} } } ``` An ID already in use is refused. Inspect that mock before choosing a new ID or replacing it with `replace_mock`. 3. Call `list_requests` with `{"limit": 0}` and remember its `mark`. This keeps earlier traffic out of the check. 4. Point the application at the mock URL returned by `health`, or exercise the mock directly: ```sh curl '/hello' # hello ``` 5. Call `verify_requests` with the check below, replacing `41` with the mark from step 3: ```json {"check": {"mock_id": "agent-hello", "after": 41, "count": 1, "within": "2s"}} ``` Read `passed` in the verdict. A failed check returns a verdict too, with a report and requests that came closest; `get_request` reads a request in detail. See [Verifying over MCP](https://vlatte.dev/verify/admin-api.md#mcp). 6. To keep the mocks added through the admin API or MCP for later runs, call `save_mocks` with `{"dir": "mocks", "format": "yaml"}`. It returns the paths it wrote, in the directory the setup command's `--mock-files=mocks/*.yaml` pattern loads. Review those files before committing them. [Mock files and the CLI](https://vlatte.dev/guides/mock-files-and-the-cli.md) shows how to serve and reload them. ## What an agent sees A tool is an admin API operation, named after it, and answers what its route answers, or its problem document as the tool's error. An agent sees only the tools the server can do: a gRPC server has no `mock_schema`, and one without a journal no `query_requests`. Tools that only read say so, and tools that remove mocks or requests are marked destructive, so an agent can ask before it calls them. Over HTTP, MCP needs the credentials the rest of the admin API needs: see [Safety](https://vlatte.dev/admin-api/index.md#safety). Over stdio it needs none: whoever holds the pipes started the program. --- Source: https://vlatte.dev/mcp/setup.md # MCP setup Connect an agent to vlatte over stdio or Streamable HTTP, from the CLI or from Go. What an agent can do with it is in the [MCP overview](https://vlatte.dev/mcp/index.md), and every tool is in [MCP tools](https://vlatte.dev/mcp/tools.md). With `admin.MCP()`, the admin API serves its operations to AI agents too, over the [Model Context Protocol](https://modelcontextprotocol.io): a tool per operation, named after it, such as `add_mock`, `list_mocks` and `query_requests`; the streams, the OpenAPI document and the pages have none. A tool's arguments carry what its route takes, the body under a key, such as `{"mock": ...}` for `add_mock` and `{"query": ...}` for `query_requests`, and its tool's schema names them. A tool answers what its route answers, or its problem document as the tool's error. An agent sees only the tools the server can do: a gRPC server has no `mock_schema`, and one without a journal no `query_requests`. `verify_requests` [checks the requests](https://vlatte.dev/verify/admin-api.md#mcp) as a Go test does, with the check under `check`. ## stdio For an agent that starts the mock server itself, configure it to launch `vlatte mcp`. Install the [CLI](https://vlatte.dev/getting-started/installation.md#the-cli) first, and run it from the project directory that holds the mock files: ```json { "mcpServers": { "vlatte": { "command": "vlatte", "args": ["mcp", "--mock-files=mocks/*.yaml"] } } } ``` The command takes the server's flags and writes its banner and logs to stderr. MCP over stdio needs no credentials: whoever holds the pipes started the program. From an error-returning Go function with a context `ctx`, serve the protocol on stdin and stdout: ```go api := admin.MustNew(admin.MCP()) m := mockhttp.MustNew(mockhttp.Setup().Admin(api).Journal("").LogOutput(os.Stderr)) m.MustStart() defer m.Close() if err := api.ServeMCP(ctx, os.Stdin, os.Stdout); err != nil { return err } ``` `ServeMCP` runs until stdin ends or the context is canceled. Only MCP writes to stdout: send your program's logs and diagnostics to stderr too. ## Streamable HTTP For an agent connecting to an existing server, start the command with known addresses: ```sh vlatte --addr=127.0.0.1:8080 --admin-addr=127.0.0.1:8081 --mock-files='mocks/*.yaml' ``` Configure the MCP client's Streamable HTTP connection with `http://127.0.0.1:8081/mcp`. This is the admin address; `http://127.0.0.1:8080` serves the mocked API your application calls. The client configuration format depends on the agent. In a Go program or test, the server's owner enables MCP and keeps the server alive for the agent's work: ```go api := admin.MustNew(admin.MCP()) m := mockhttp.MustNew(mockhttp.Setup().Admin(api).Journal("")) m.MustStart() defer m.Close() fmt.Println(m.AdminURL() + "/mcp") // ... run the application or test while the agent connects to this URL ... ``` `POST /mcp` keeps no sessions and answers in JSON. It needs the credentials the rest of the admin API needs; for bearer authentication, the MCP client sends `Authorization: Bearer `. See [Safety](https://vlatte.dev/admin-api/index.md#safety). ## Available tools and disabling MCP The CLI serves MCP on the admin API by default. In Go, `admin.MCP()` enables it. The examples above also enable a [journal](https://vlatte.dev/more/journal.md), which makes the request inspection and verification tools available. `mcp: false` (`--mcp=false`, `VLATTE_MCP=false`) turns MCP off in the CLI. The [tool reference](https://vlatte.dev/mcp/tools.md) lists which tools each server offers, and the [workflow](https://vlatte.dev/mcp/index.md#inspect-add-and-verify) shows how to use them together. --- Source: https://vlatte.dev/mcp/tools.md # MCP tools Each tool runs an [admin API](https://vlatte.dev/admin-api/index.md) operation. A tool's arguments carry what its route takes, the body under a key, such as `{"mock": ...}` for `add_mock` and `{"query": ...}` for `query_requests`, and its schema names them. The server lists only the tools it can do: the last column says when a tool is there. A server without a journal has none of the request tools, and a gRPC server has no `mock_schema`. ## Server | Tool | Route | What it does | Listed when | |---|---|---|---| | `health` | `GET /health` | Tells the server's kind, such as `http`, and the URL it serves its mocks at. | Always | | `reset` | `POST /reset` | Removes the mocks the admin API added, loads the mock files again, starts every scenario over and clears the journal. Mocks added in code, and the key-value store, stay. | Always | ## Mocks | Tool | Route | What it does | Listed when | |---|---|---|---| | `list_mocks` | `GET /mocks` | Lists every mock, enabled or not, in the order mocks match requests, with its priority, hits and origin. | Always | | `get_mock` | `GET /mocks/{id}` | Gets a mock as a mock file describes it, which `replace_mock` takes back. | Always | | `mock_schema` | `GET /schema` | Gets the JSON Schema of the server's mock files. | HTTP servers | | `validate_mock` | `POST /mocks/validate` | Checks a mock as `add_mock` would, without adding it, and lists its problems. | Always | | `add_mock` | `POST /mocks` | Adds a mock, which the server serves at once. An id in use is refused. | Always | | `replace_mock` | `PUT /mocks/{id}` | Replaces a mock in one step: a request finds the old mock or the new one, never neither. | Always | | `update_mock` | `PATCH /mocks/{id}` | Enables or disables a mock. A disabled mock matches no request, and keeps its hits. | Always | | `remove_mock` | `DELETE /mocks/{id}` | Removes a mock. | Always | | `remove_all_mocks` | `DELETE /mocks` | Removes every mock, of mock files and code too. | Always | | `reload_mocks` | `POST /mocks/reload` | Loads the mock files, and the mocks the admin API keeps, again. | Always | | `save_mocks` | `POST /mocks/save` | Writes the mocks the admin API added as mock files, and lists their paths. | Always | ## Recording | Tool | Route | What it does | Listed when | |---|---|---|---| | `start_recording` | `POST /recording` | Starts a recording: requests go to its target, and their answers become mock files once it stops. | Always | | `recording_status` | `GET /recording` | Counts the recording's calls and the mocks it wrote, or tells that none runs. | Always | | `stop_recording` | `DELETE /recording` | Stops the recording, and lists the mock files it wrote, which the server serves from then on. | Always | See [Recording through the admin API](https://vlatte.dev/recording/admin-api.md). ## Scenarios | Tool | Route | What it does | Listed when | |---|---|---|---| | `list_scenarios` | `GET /scenarios` | Lists every scenario: its state, the states its mocks name, and which mock moves it where. | Always | | `set_scenario` | `PUT /scenarios/{name}` | Moves a scenario to a state. A state none of its mocks names is refused. | Always | | `reset_scenario` | `DELETE /scenarios/{name}` | Moves one scenario back to where it started: every instance of it, or the one of a key. | Always | | `reset_scenarios` | `DELETE /scenarios` | Moves every scenario back to where it started. | Always | See [Scenarios](https://vlatte.dev/scenarios/index.md). ## Key-value store | Tool | Route | What it does | Listed when | |---|---|---|---| | `list_kv` | `GET /kv` | Lists the keys of the key-value store templates read, in order, without their values. | Always | | `get_kv` | `GET /kv/{key}` | Gets the value of a key. A key that isn't set is refused. | Always | | `set_kv` | `PUT /kv/{key}` | Sets a key to a value, which templates read from the next request on. | Always | | `delete_kv` | `DELETE /kv/{key}` | Removes a key. A key that isn't set is no error. | Always | | `reload_kv` | `POST /kv/reload` | Loads the store again from its file, in place of every value. A file with a problem changes nothing. | The store loads from a file, as the command's `kv.file` does | See [Key-value store](https://vlatte.dev/templating/index.md#key-value-store). ## Requests | Tool | Route | What it does | Listed when | |---|---|---|---| | `list_requests` | `GET /requests` | Lists the newest requests of the journal, newest first, a page at a time. | The server keeps a journal | | `get_request` | `GET /requests/{id}` | Gets a request, with its answer and the mock that answered it. | The server keeps a journal | | `query_requests` | `QUERY /requests` | Finds and counts the requests a query selects, such as those a mock's request part matches. | The server keeps a journal | | `verify_requests` | `QUERY /requests/verify` | Checks that the journal holds the requests a test expects: how many, by when, or in what order. A failed check says why. | The server keeps a journal | | `clear_requests` | `DELETE /requests` | Removes every request of the journal. | The server keeps a journal | `verify_requests` takes the check under `check`: see [Verifying over MCP](https://vlatte.dev/verify/admin-api.md#mcp) and the [request journal](https://vlatte.dev/more/journal.md). --- Source: https://vlatte.dev/reference/index.md # Reference Find Go API contracts, mock file schemas, the Admin API OpenAPI reference and the complete CLI settings. | Reference | What it holds | |---|---| | [Go API](https://vlatte.dev/reference/go-api.md) | The packages, on pkg.go.dev. | | [Mock file schemas](https://vlatte.dev/reference/schemas.md) | The JSON Schemas of HTTP and gRPC mock files, at stable addresses, and how to use them in an editor. | | [Admin API (OpenAPI)](https://vlatte.dev/reference/admin-api.html){target="_blank" rel="noopener noreferrer"} | Every route of the admin API, from its OpenAPI document. | | [Settings](https://vlatte.dev/reference/settings.md) | Every setting of the `vlatte` command, with its flag, variable and file key. | ## Stable addresses These files are served as they are, for editors, generators and mock files to point at: | What | Address | |---|---| | HTTP mock file schema | `https://vlatte.dev/schemas/http.schema.json` | | gRPC mock file schema | `https://vlatte.dev/schemas/grpc.schema.json` | | Admin API OpenAPI document | `https://vlatte.dev/openapi/admin.yaml` | They follow the `main` branch of the repository. --- Source: https://vlatte.dev/reference/go-api.md # Go API The Go API is documented on pkg.go.dev, from the doc comments of the code. Packages not yet published there link to their source: | Package | Docs | |---|---| | `vlatte` | [pkg.go.dev/github.com/caffeine-projects/vlatte](https://pkg.go.dev/github.com/caffeine-projects/vlatte) | | `mockhttp` | [pkg.go.dev/github.com/caffeine-projects/vlatte/mockhttp](https://pkg.go.dev/github.com/caffeine-projects/vlatte/mockhttp) | | `mockgrpc` | [pkg.go.dev/github.com/caffeine-projects/vlatte/mockgrpc](https://pkg.go.dev/github.com/caffeine-projects/vlatte/mockgrpc) | | `match` | [pkg.go.dev/github.com/caffeine-projects/vlatte/match](https://pkg.go.dev/github.com/caffeine-projects/vlatte/match) | | `match/matchbuild` | [pkg.go.dev/github.com/caffeine-projects/vlatte/match/matchbuild](https://pkg.go.dev/github.com/caffeine-projects/vlatte/match/matchbuild) | | `match/matchscenario` | [pkg.go.dev/github.com/caffeine-projects/vlatte/match/matchscenario](https://pkg.go.dev/github.com/caffeine-projects/vlatte/match/matchscenario) | | `match/matchrepeat` | [pkg.go.dev/github.com/caffeine-projects/vlatte/match/matchrepeat](https://pkg.go.dev/github.com/caffeine-projects/vlatte/match/matchrepeat) | | `fault` | [pkg.go.dev/github.com/caffeine-projects/vlatte/fault](https://pkg.go.dev/github.com/caffeine-projects/vlatte/fault) | | `fault/faultbuild` | [pkg.go.dev/github.com/caffeine-projects/vlatte/fault/faultbuild](https://pkg.go.dev/github.com/caffeine-projects/vlatte/fault/faultbuild) | | `webhook` | [pkg.go.dev/github.com/caffeine-projects/vlatte/webhook](https://pkg.go.dev/github.com/caffeine-projects/vlatte/webhook) | | `journal` | [pkg.go.dev/github.com/caffeine-projects/vlatte/journal](https://pkg.go.dev/github.com/caffeine-projects/vlatte/journal) | | `verify` | [Source](https://github.com/caffeine-projects/vlatte/tree/main/verify) | | `admin` | [pkg.go.dev/github.com/caffeine-projects/vlatte/admin](https://pkg.go.dev/github.com/caffeine-projects/vlatte/admin) | | `recipe/http/proxyall` | [pkg.go.dev/github.com/caffeine-projects/vlatte/recipe/http/proxyall](https://pkg.go.dev/github.com/caffeine-projects/vlatte/recipe/http/proxyall) | | `recipe/http/githubauth` | [Source](https://github.com/caffeine-projects/vlatte/tree/main/recipe/http/githubauth) | | `recipe/http/oidc` | [Source](https://github.com/caffeine-projects/vlatte/tree/main/recipe/http/oidc) | --- Source: https://vlatte.dev/reference/schemas.md # Mock file schemas Mock files have JSON Schemas, served at stable addresses, so editors complete and check them as you write: | Mock files | Schema | |---|---| | HTTP | [`https://vlatte.dev/schemas/http.schema.json`](https://vlatte.dev/schemas/http.schema.json){target="_self"} | | gRPC | [`https://vlatte.dev/schemas/grpc.schema.json`](https://vlatte.dev/schemas/grpc.schema.json){target="_self"} | Each schema's `$id` is its address. They describe the format the decoder reads: tests keep the schemas in step with it. The server never reads them; a file a schema accepts can still fail to load, such as one that names a body file that isn't there, and the error says why. The admin API serves the HTTP schema too, at `GET /schema`. ## YAML Start a YAML mock file with a comment naming its schema: ```yaml # yaml-language-server: $schema=https://vlatte.dev/schemas/http.schema.json request: method: GET path: /hello response: status: 200 body: hello world ``` Editors that use the YAML language server read it: VS Code with the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml), Neovim, Helix, Zed and others. JetBrains IDEs read it too. To map the schemas by file name instead, without a comment in each file, add them to the settings of the YAML extension in VS Code: ```json { "yaml.schemas": { "https://vlatte.dev/schemas/http.schema.json": ["**/_mocks/*mock.yaml", "**/_mocks/*mock.yml"], "https://vlatte.dev/schemas/grpc.schema.json": ["**/grpc/*.yaml"] } } ``` ## JSON Name the schema in a JSON mock file's `$schema` key. The server skips the key: ```json { "$schema": "https://vlatte.dev/schemas/http.schema.json", "request": {"method": "GET", "path": "/hello"}, "response": {"status": 200, "body": "hello"} } ``` To map the schemas by file name instead, use `json.schemas` in VS Code: ```json { "json.schemas": [ {"fileMatch": ["**/_mocks/*mock.json"], "url": "https://vlatte.dev/schemas/http.schema.json"} ] } ``` In JetBrains IDEs, add the address under **Settings › Languages & Frameworks › Schemas and DTDs › JSON Schema Mappings**. --- Source: https://vlatte.dev/reference/settings.md # Settings Every setting of the `vlatte` command, as `vlatte --help` lists it: its flag, its environment variable and its key in a `.vlatterc` file. See [Configuration](https://vlatte.dev/run/configuration.md) for the order sources apply in and how values are written. Servers made in Go read the same settings from a `.vlatterc` file and `VLATTE_*` variables, except those of the command only, such as `https` and `admin`: see [Configuration](https://vlatte.dev/run/configuration.md#sources-in-tests). ```txt Settings: --admin Serve the admin API, on an HTTP server of its own. Default: true. env VLATTE_ADMIN, file key admin --admin-addr string Address the admin API listens on: host:port, :port or port. Empty picks a random local port. env VLATTE_ADMIN_ADDR, file key admin.addr --admin-dir string Directory of the files that keep the mocks the admin API adds across restarts, which only the server loads: keep it out of mock_files. Relative paths start at root_dir, and {workdir} at workdir. Empty keeps them in memory. env VLATTE_ADMIN_DIR, file key admin.dir --admin-docs-version string Version of Scalar, such as 1.80.0, that the documentation page of the admin APIs, at /docs, loads instead of the one vlatte pins, which it checks against its hash. Empty is the pinned one. env VLATTE_ADMIN_DOCS_VERSION, file key admin.docs_version --admin-auth-username string Username the admin APIs ask every request for, but their probes, with admin.auth.password: HTTP Basic authentication, which browsers show a sign-in prompt for. Serve them over TLS beyond the local host. env VLATTE_ADMIN_AUTH_USERNAME, file key admin.auth.username VLATTE_ADMIN_AUTH_PASSWORD string Password of admin.auth.username. Or admin.auth.password_file. file key admin.auth.password; no flag, as the command line shows to every user of the host --admin-auth-password-file string File that holds the password of admin.auth.username, such as a Docker or Kubernetes secret. A trailing line break isn't part of it. env VLATTE_ADMIN_AUTH_PASSWORD_FILE, file key admin.auth.password_file VLATTE_ADMIN_AUTH_TOKEN string Bearer token the admin APIs ask every request for, but their probes: in Authorization: Bearer, or as the password of Basic, with any username, from a browser. Generate one with openssl rand -hex 32. Or admin.auth.token_file. file key admin.auth.token; no flag, as the command line shows to every user of the host --admin-auth-token-file string File that holds the bearer token of the admin APIs, such as a Docker or Kubernetes secret. A trailing line break isn't part of it. env VLATTE_ADMIN_AUTH_TOKEN_FILE, file key admin.auth.token_file --admin-tls-cert string Certificate file, PEM, the admin APIs serve HTTPS with, with admin.tls.key. env VLATTE_ADMIN_TLS_CERT, file key admin.tls.cert --admin-tls-key string Private key file, PEM, of admin.tls.cert. env VLATTE_ADMIN_TLS_KEY, file key admin.tls.key --mcp Serve the admin API's operations to AI agents over MCP: at POST /mcp of the admin API, and over stdio with vlatte mcp. Default: true. env VLATTE_MCP, file key mcp --webhook-timeout duration How long a webhook without a timeout of its own waits for its answer, for HTTP and gRPC mocks: milliseconds or a Go duration (1.5s). 0 uses 10s. env VLATTE_WEBHOOK_TIMEOUT, file key webhook.timeout --kv-file string CSV file to fill the key-value store with, which templates of HTTP and gRPC mocks read as .KV: the header key,value, then a row for each key. Changes to the store stay in memory, never in the file. Empty starts the store empty. env VLATTE_KV_FILE, file key kv.file --grpc-admin Serve the admin API of the gRPC server, on an HTTP server of its own. Default: true. env VLATTE_GRPC_ADMIN, file key grpc.admin --grpc-admin-addr string Address the gRPC server's admin API listens on: host:port, :port or port. Empty picks a random local port. env VLATTE_GRPC_ADMIN_ADDR, file key grpc.admin.addr --grpc-admin-dir string Directory of the files that keep the mocks the gRPC server's admin API adds across restarts, which only the server loads: keep it out of grpc.mock_files. Relative paths start at grpc.root_dir, and {workdir} at workdir. Empty keeps them in memory. env VLATTE_GRPC_ADMIN_DIR, file key grpc.admin.dir -c, --config string Config file to load, instead of looking up .vlatterc.{json,yaml,yml} in . and testdata. env VLATTE_CONFIG, file key config -n, --name string Server name, shown in logs. It names the folder of the server's files in testdata/vlatte, unless workdir is set: a path inside it, which doesn't start with _. env VLATTE_NAME, file key name -a, --addr string Address to listen on: host:port, :port or port. Empty picks a random local port. env VLATTE_ADDR, file key addr --root-dir string Directory that relative mock file patterns and recording paths start at. env VLATTE_ROOT_DIR, file key root_dir --workdir string Directory of the server's own files: its mock files, recordings, admin mocks and journal. A path setting that starts with {workdir} starts there. Relative paths start at root_dir. Default: testdata/vlatte, or testdata/vlatte/ for a named server. env VLATTE_WORKDIR, file key workdir --mock-files list Glob patterns of the mock files to load on start. Default: {workdir}/_mocks/*mock.yaml,{workdir}/_mocks/*mock.yml,{workdir}/_mocks_recorded/*.mock.json,{workdir}/_mocks_recorded/*.mock.yaml,{workdir}/_mocks_recorded/*.mock.yml. env VLATTE_MOCK_FILES, file key mock_files --no-match-status-code int Status code of the response to a request that matches no mock. Default: 418. env VLATTE_NO_MATCH_STATUS_CODE, file key no_match_status_code --no-match-format name Format of the response to a request that matches no mock. auto follows its Accept header. One of: auto, text, markdown, json, problem. Default: auto. env VLATTE_NO_MATCH_FORMAT, file key no_match_format --error-status-code int Status code of the response when serving a request fails, such as a reply that fails. Default: 418. env VLATTE_ERROR_STATUS_CODE, file key error_status_code --https Serve HTTPS instead of HTTP. Only the vlatte command reads it. env VLATTE_HTTPS, file key https --colors Use colors in debug logs. Default: true. env VLATTE_COLORS, file key colors --debug Print descriptive logs for each request, instead of structured ones. env VLATTE_DEBUG, file key debug --debug-headers Add debug headers, such as VLatte-Matched-Mock-Id, to each answer. VLatte-Error stays either way. Default: true. env VLATTE_DEBUG_HEADERS, file key debug_headers --near-misses List the mocks closest to a request no mock matched in its answer. It runs the matchers matching never reached. Default: true. env VLATTE_NEAR_MISSES, file key near_misses --tls-cert string TLS certificate file. Requires tls.key. env VLATTE_TLS_CERT, file key tls.cert --tls-key string TLS private key file. Requires tls.cert. env VLATTE_TLS_KEY, file key tls.key --tls-ca string File of the CA certificates that client certificates must be signed by (mutual TLS). Requires tls.cert and tls.key. env VLATTE_TLS_CA, file key tls.ca --log-level name Log level. One of: trace, debug, info, warn, error, fatal, panic, none, disabled. Default: info. env VLATTE_LOG_LEVEL, file key log.level --log-verbosity name How much of each request and response to log. One of: basic, header, body. Default: header. env VLATTE_LOG_VERBOSITY, file key log.verbosity --log-max-body-size int Bodies larger than this many bytes aren't logged. 0 logs bodies of any size. env VLATTE_LOG_MAX_BODY_SIZE, file key log.max_body_size --log-pretty Human-friendly console logs instead of JSON. Default: true. env VLATTE_LOG_PRETTY, file key log.pretty --cors Enable CORS. env VLATTE_CORS, file key cors --cors-allowed-origin string Allowed origins, comma-separated. * allows any. Default: *. env VLATTE_CORS_ALLOWED_ORIGIN, file key cors.allowed_origin --cors-allow-credentials Send Access-Control-Allow-Credentials: true. With *, the origin of the request is allowed. env VLATTE_CORS_ALLOW_CREDENTIALS, file key cors.allow_credentials --cors-allowed-methods string Allowed methods, comma-separated. * allows the method a preflight request asks for. Default: GET,POST,HEAD,PUT,DELETE,PATCH. env VLATTE_CORS_ALLOWED_METHODS, file key cors.allowed_methods --cors-allowed-headers string Allowed headers, comma-separated. Empty or * allows the headers a preflight request asks for. env VLATTE_CORS_ALLOWED_HEADERS, file key cors.allowed_headers --cors-expose-headers string Headers exposed to browsers, comma-separated. env VLATTE_CORS_EXPOSE_HEADERS, file key cors.expose_headers --cors-max-age int Seconds browsers may cache a preflight response. -1 omits Access-Control-Max-Age. Default: 0. env VLATTE_CORS_MAX_AGE, file key cors.max_age --cors-success-status-code int Status code of the response to a preflight request. Browsers fail a preflight answered with a status other than 2xx. Default: 204. env VLATTE_CORS_SUCCESS_STATUS_CODE, file key cors.success_status_code --proxy Proxy the requests that match no mock to their original host. env VLATTE_PROXY, file key proxy --proxy-via string URL of a proxy to send proxied requests through. env VLATTE_PROXY_VIA, file key proxy.via --proxy-timeout duration How long to wait for the response headers of a proxied request: milliseconds or a Go duration (1.5s). 0 waits forever. Default: 30s. env VLATTE_PROXY_TIMEOUT, file key proxy.timeout --proxy-ssl-verify Verify the TLS certificates of proxied servers. env VLATTE_PROXY_SSL_VERIFY, file key proxy.ssl_verify --proxy-intercept Serve the HTTPS of CONNECT tunnels with certificates signed by proxy.ca_cert, so mocks answer it. Requires proxy.ca_cert and proxy.ca_key. env VLATTE_PROXY_INTERCEPT, file key proxy.intercept --proxy-ca-cert string Certificate file of the CA that signs the certificates proxy.intercept serves, which clients must trust. Requires proxy.ca_key. env VLATTE_PROXY_CA_CERT, file key proxy.ca_cert --proxy-ca-key string Private key file of proxy.ca_cert. Requires proxy.ca_cert. env VLATTE_PROXY_CA_KEY, file key proxy.ca_key --proxy-all Proxy the requests no mock matches, but those the forward proxy sends on, to proxy_all.target. env VLATTE_PROXY_ALL, file key proxy_all --proxy-all-target string URL of the server to forward requests to. Its path and query come before the request's. env VLATTE_PROXY_ALL_TARGET, file key proxy_all.target --proxy-all-remove-request-headers list Headers removed from the forwarded request. env VLATTE_PROXY_ALL_REMOVE_REQUEST_HEADERS, file key proxy_all.remove_request_headers --proxy-all-trim-prefix string Prefix of whole segments removed from the request path, before the target's path is added. env VLATTE_PROXY_ALL_TRIM_PREFIX, file key proxy_all.trim_prefix --proxy-all-trim-suffix string Text removed from the end of the request path, such as an extension, before the target's path is added. env VLATTE_PROXY_ALL_TRIM_SUFFIX, file key proxy_all.trim_suffix --proxy-all-timeout duration How long to wait for the target's response headers, connecting and sending the request included: milliseconds or a Go duration (1.5s). 0 waits forever. Default: 30s. env VLATTE_PROXY_ALL_TIMEOUT, file key proxy_all.timeout --proxy-all-ssl-verify Verify the TLS certificate of the target. Off by default. env VLATTE_PROXY_ALL_SSL_VERIFY, file key proxy_all.ssl_verify --proxy-all-x-forwarded Set X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto on the forwarded request, with the client's address, host and scheme. env VLATTE_PROXY_ALL_X_FORWARDED, file key proxy_all.x_forwarded --proxy-all-preserve-host Send the target the Host the client asked for, in place of the target's. env VLATTE_PROXY_ALL_PRESERVE_HOST, file key proxy_all.preserve_host --record Record proxied requests and their responses as mock files, from start to close. env VLATTE_RECORD, file key record --record-dir string Directory of the recordings. Relative paths start at root_dir, and {workdir} at workdir. Default: {workdir}/_mocks_recorded. env VLATTE_RECORD_DIR, file key record.dir --record-format string Format of the recorded mock files: json, yaml, yml. Default: json. env VLATTE_RECORD_FORMAT, file key record.format --record-target string Server to record, such as https://api.example.com: the requests the recording takes go to it, before any mock. env VLATTE_RECORD_TARGET, file key record.target --record-skip-ssl-verify Skip the verification of the TLS certificate of record.target, such as for a self-signed one. env VLATTE_RECORD_SKIP_SSL_VERIFY, file key record.skip_ssl_verify --record-methods list Methods of the requests the recording takes. Empty takes every method. env VLATTE_RECORD_METHODS, file key record.methods --record-paths list Glob patterns of the paths of the requests the recording takes, such as /v1/*, where * matches / too. Empty takes every path. env VLATTE_RECORD_PATHS, file key record.paths --record-request-headers list Request headers recordings match: the first value each had, or its absence. env VLATTE_RECORD_REQUEST_HEADERS, file key record.request_headers --record-exclude-response-headers list Response headers recordings leave out. A list replaces the default; an empty one keeps every header. Default: Set-Cookie. env VLATTE_RECORD_EXCLUDE_RESPONSE_HEADERS, file key record.exclude_response_headers --record-body-files Write each recorded response body to a file of its own, instead of inline. env VLATTE_RECORD_BODY_FILES, file key record.body_files --record-max-inline-body int Bytes of a body kept in memory while it is recorded: a larger one goes to a file as it streams. Text bodies kept in memory are written inline. 0 uses the default. Default: 1048576. env VLATTE_RECORD_MAX_INLINE_BODY, file key record.max_inline_body --record-max-body-size int Calls with a larger response body, in bytes, aren't recorded. 0 uses the default; a negative value means no limit. Default: 1073741824. env VLATTE_RECORD_MAX_BODY_SIZE, file key record.max_body_size --record-repeats name What a recording keeps of a request that comes again: every response, in order, or the first one. One of: sequence, first. Default: sequence. env VLATTE_RECORD_REPEATS, file key record.repeats --record-include-mocked Record the answers of mocks too, not only proxied ones. env VLATTE_RECORD_INCLUDE_MOCKED, file key record.include_mocked --journal Keep a journal of the requests the server serves, of every outcome. env VLATTE_JOURNAL, file key journal --journal-dir string Directory of the journal's files, kept across restarts. Relative paths start at root_dir, and {workdir} at workdir. Empty keeps them in a temporary directory, removed when the server stops. env VLATTE_JOURNAL_DIR, file key journal.dir --journal-max-entries int How many requests the journal keeps, the newest. 0 keeps them all. Default: 10000. env VLATTE_JOURNAL_MAX_ENTRIES, file key journal.max_entries --journal-max-body-size int How many bytes of a request's body the journal keeps. 0 keeps whole bodies. Default: 1048576. env VLATTE_JOURNAL_MAX_BODY_SIZE, file key journal.max_body_size --template-env list Environment variables templates may read with env, by exact name. Templates read none without it. env VLATTE_TEMPLATE_ENV, file key template.env --template-keys list Keys templates sign tokens with, jwt, and publish, jwks: kid=ALG generates one at start, such as google-1=RS256, and kid=ALG:path reads a PEM private key, relative paths starting at root_dir. env VLATTE_TEMPLATE_KEYS, file key template.keys --outbound-allow list Address ranges the server may connect to, in CIDR form, such as 10.1.0.0/16: proxied requests, the recording target, the forward proxy and webhooks reach no other address. Empty allows every address. env VLATTE_OUTBOUND_ALLOW, file key outbound.allow --outbound-deny list Address ranges the server never connects to, in CIDR form, such as 169.254.0.0/16. Deny wins over outbound.allow. env VLATTE_OUTBOUND_DENY, file key outbound.deny gRPC settings: --config string Config file to read, in place of .vlatterc: the HTTP server's, whose grpc section this is. env VLATTE_CONFIG, file key config --grpc Serve gRPC mocks: the vlatte command serves them when the grpc section is on. env VLATTE_GRPC, file key grpc --grpc-name string Name of the gRPC server, in its log lines. env VLATTE_GRPC_NAME, file key grpc.name --grpc-addr string Address the gRPC server listens on: host:port or :port. Default: 127.0.0.1:0. env VLATTE_GRPC_ADDR, file key grpc.addr --grpc-root-dir string Directory relative mock_files patterns start at. Empty is the working directory. env VLATTE_GRPC_ROOT_DIR, file key grpc.root_dir --grpc-mock-files list Glob patterns of gRPC mock files, loaded at start. env VLATTE_GRPC_MOCK_FILES, file key grpc.mock_files --grpc-descriptors list Descriptor set files of the services to mock, such as protoc --descriptor_set_out --include_imports writes. env VLATTE_GRPC_DESCRIPTORS, file key grpc.descriptors --grpc-reflection Serve gRPC server reflection, so grpcurl and other tools list the services. env VLATTE_GRPC_REFLECTION, file key grpc.reflection --grpc-template-env list Environment variables templates may read with env, by exact name. Templates read none without it. env VLATTE_GRPC_TEMPLATE_ENV, file key grpc.template.env --grpc-template-keys list Keys templates sign tokens with, jwt, and publish, jwks: kid=ALG generates one at start, such as google-1=RS256, and kid=ALG:path reads a PEM private key, relative paths starting at grpc.root_dir. env VLATTE_GRPC_TEMPLATE_KEYS, file key grpc.template.keys --grpc-outbound-allow list Address ranges the gRPC server may connect to, in CIDR form, such as 10.1.0.0/16: proxied calls, the recording target and webhooks reach no other address. Empty allows every address. env VLATTE_GRPC_OUTBOUND_ALLOW, file key grpc.outbound.allow --grpc-outbound-deny list Address ranges the gRPC server never connects to, in CIDR form, such as 169.254.0.0/16. Deny wins over grpc.outbound.allow. env VLATTE_GRPC_OUTBOUND_DENY, file key grpc.outbound.deny --grpc-tls Serve TLS. Only the vlatte command reads it: in Go, Server.StartTLS serves TLS. env VLATTE_GRPC_TLS, file key grpc.tls --grpc-tls-cert string TLS certificate file. Requires grpc.tls.key. Without one, TLS serves a self-signed certificate. env VLATTE_GRPC_TLS_CERT, file key grpc.tls.cert --grpc-tls-key string TLS private key file. Requires grpc.tls.cert. env VLATTE_GRPC_TLS_KEY, file key grpc.tls.key --grpc-tls-ca string File of the CA certificates that client certificates must be signed by (mutual TLS). Requires grpc.tls.cert and grpc.tls.key. env VLATTE_GRPC_TLS_CA, file key grpc.tls.ca --grpc-no-match-code string Code of the answer to a call no mock matched: its name, such as UNIMPLEMENTED, or its number. Default: NOT_FOUND. env VLATTE_GRPC_NO_MATCH_CODE, file key grpc.no_match_code --grpc-debug-headers Add the debug header metadata, such as vlatte-matched-mock-id, to the server's answers. Default: true. env VLATTE_GRPC_DEBUG_HEADERS, file key grpc.debug_headers --grpc-near-misses Journal the mocks closest to a call no mock matched. Default: true. env VLATTE_GRPC_NEAR_MISSES, file key grpc.near_misses --grpc-shutdown-timeout duration How long the server waits for the calls in flight when it stops: milliseconds or a Go duration (1.5s). Default: 5s. env VLATTE_GRPC_SHUTDOWN_TIMEOUT, file key grpc.shutdown_timeout --grpc-log-level name Level of the gRPC server's log. One of: trace, debug, info, warn, error, fatal, panic, none, disabled. Default: info. env VLATTE_GRPC_LOG_LEVEL, file key grpc.log.level --grpc-journal Keep a journal of the calls the gRPC server serves, of every outcome. env VLATTE_GRPC_JOURNAL, file key grpc.journal --grpc-journal-dir string Directory of the journal's files, kept across restarts. Empty keeps them in a temporary directory, removed when the server stops. env VLATTE_GRPC_JOURNAL_DIR, file key grpc.journal.dir --grpc-journal-max-entries int How many calls the journal keeps, the newest. 0 keeps them all. Default: 10000. env VLATTE_GRPC_JOURNAL_MAX_ENTRIES, file key grpc.journal.max_entries --grpc-journal-max-body-size int How many bytes of a call's request, as JSON, the journal keeps. 0 keeps whole requests. Default: 1048576. env VLATTE_GRPC_JOURNAL_MAX_BODY_SIZE, file key grpc.journal.max_body_size --grpc-record Record the calls the gRPC server proxies as mock files, from start to close. env VLATTE_GRPC_RECORD, file key grpc.record --grpc-record-dir string Directory of the gRPC recordings. Relative paths start at grpc.root_dir. Default: _mocks_recorded. env VLATTE_GRPC_RECORD_DIR, file key grpc.record.dir --grpc-record-format string Format of the recorded gRPC mock files: json, yaml, yml. Default: json. env VLATTE_GRPC_RECORD_FORMAT, file key grpc.record.format --grpc-record-target string gRPC server to record, such as localhost:50051: the calls the recording takes go to it, without TLS, before any mock. env VLATTE_GRPC_RECORD_TARGET, file key grpc.record.target --grpc-record-methods list Methods of the calls the recording takes, by whole segments of their full method, such as Service/Method. Empty takes every method. env VLATTE_GRPC_RECORD_METHODS, file key grpc.record.methods --grpc-record-request-headers list Metadata keys recordings match: the value each had, or its absence. env VLATTE_GRPC_RECORD_REQUEST_HEADERS, file key grpc.record.request_headers --grpc-record-repeats name What a recording keeps of a request that comes again: every answer, in order, or the first one. One of: sequence, first. Default: sequence. env VLATTE_GRPC_RECORD_REPEATS, file key grpc.record.repeats --grpc-record-include-mocked Record the answers of mocks too, not only proxied ones. env VLATTE_GRPC_RECORD_INCLUDE_MOCKED, file key grpc.record.include_mocked ``` --- Source: https://vlatte.dev/contributing.md # Contributing VLatte is developed on [GitHub](https://github.com/caffeine-projects/vlatte). Bug reports, ideas and pull requests are welcome. ## Pull requests Read [CONTRIBUTING.md](https://github.com/caffeine-projects/vlatte/blob/main/CONTRIBUTING.md) before you open one. It covers setup, the checks CI runs, the conventions reviewers look for, and the commit format. ## Issues Open an issue with one of the [issue forms](https://github.com/caffeine-projects/vlatte/issues/new/choose): - **Bug report:** include the VLatte version, what you expected, what happened, and the smallest mock or test that reproduces it. - **Feature request:** describe the problem it solves before the API you have in mind. - **Other:** documentation issues and usage questions. Search the [existing issues](https://github.com/caffeine-projects/vlatte/issues) first. ## Security Do not report a vulnerability in a public issue. Report it privately through [GitHub security advisories](https://github.com/caffeine-projects/vlatte/security/advisories/new), as the [security policy](https://github.com/caffeine-projects/vlatte/blob/main/SECURITY.md) explains. Only the latest release gets security fixes.