feat(14-01): record vendor calls with summer parity:upstream and replay them offline
- WriteUpstream masks vars, hashes long base64 JSON strings and refuses unmasked Authorization/X-Api-Key - multipart requests recorded as ordered parts; the fake compares parts and hashed payloads - loopback CONNECT recording proxy with a local ECDSA parity CA, script and forward modes - parity:upstream command, README and parity docs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Parity testing
|
||||
description: Record the reference backend's responses and broadcasts with tide, replay them against the Go port and diff them after masking IDs and timestamps.
|
||||
description: Record the reference backend's responses, broadcasts and vendor calls with tide, replay them against the Go port and diff them after masking IDs and timestamps.
|
||||
section: services
|
||||
order: 150
|
||||
---
|
||||
@@ -124,3 +124,47 @@ summer parity:broadcasts --flow testdata/broadcasts/flows/post-lifecycle.yaml --
|
||||
Point the reference backend's Centrifugo API URL at the recorder (`127.0.0.1:8424` by default). `--step` keeps only the publications of one step, running the earlier steps as setup. Timestamps, the actor, the `*_at` dates inside a published album, a notification's own `created_at` and `id` under `$.data.payload` (the id only when it is a positive integer, as `{{id}}` unless a captured ID names it) and captured IDs are masked (`tide.NormalizePublications`), so the Go port's publications, recorded the same way, compare with `tide.DiffPublications`. A golden with `--pending` set is recorded but not yet asserted.
|
||||
|
||||
On the Go side, the memory realtime driver records publications the same way in tests; see [Realtime](realtime.md).
|
||||
|
||||
## Upstream exchanges
|
||||
|
||||
Some routes call outside services: a metadata API, a model vendor, a ticketing system. Two things must hold for them: the port must send the vendor the same requests the reference backend sent, and tests must never call the vendor. Upstream sidecars cover both.
|
||||
|
||||
A sidecar sits next to its fixture as `<fixture>.upstream.yaml` (`tide.UpstreamPath`) and holds the vendor exchanges of that fixture in recorded order:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
exchanges:
|
||||
- request:
|
||||
method: POST
|
||||
url: https://api.example.com/v1/items?mode=fast
|
||||
headers:
|
||||
Authorization: Bearer {{secret:example-token}}
|
||||
Content-Type: application/json
|
||||
User-Agent: example-client/1.0
|
||||
body: '{"name":"widget"}'
|
||||
response:
|
||||
status: 201
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
body: '{"id":7}'
|
||||
```
|
||||
|
||||
Only the compared request headers are kept (`tide.UpstreamCompareHeaders`: User-Agent, Accept, Content-Type, Authorization, X-Api-Key and the model vendors' version headers). A multipart request keeps an ordered list of `parts` instead of a body, with the SHA-256 of each file instead of its bytes, and a JSON string longer than 1024 characters that is base64 (an uploaded photo, for example) is stored as `{{sha256:<hex>}}`.
|
||||
|
||||
### Recording through the proxy
|
||||
|
||||
The reference backend's vendor URLs are usually code literals, so the way to capture what it really sends is a recording HTTPS proxy. `summer parity:upstream` runs one on `127.0.0.1:8425`:
|
||||
|
||||
```sh
|
||||
summer parity:upstream --ca-dir /tmp/parity/ca --vars /tmp/parity/vars.yaml \
|
||||
--script testdata/parity/vendor-script.yaml \
|
||||
--out testdata/parity/routes/POST_items_jwt__ok.upstream.yaml
|
||||
```
|
||||
|
||||
It prints the path of its CA certificate. Start the reference backend with `HTTPS_PROXY=http://127.0.0.1:8425` and that certificate as its curl and OpenSSL CA file, run the case, then interrupt the command to write the sidecar. In the default `script` mode each request is answered from the script file, a list of `responses` entries (`tide.UpstreamScriptResponse`: `method`, `host`, `path` and the `response` to send), so recording needs no real vendor and no real credential; a request no entry matches is answered with status 599 and the sidecar is not written. `--mode forward` sends each request once to the real vendor through a guarded client instead. Use it only by hand, never in CI.
|
||||
|
||||
The rules match the other recorders: the proxy listens on loopback only, the CA key is kept with mode 0600 in `--ca-dir` outside the fixtures tree, and every value from the variables file is replaced by its `{{name}}` placeholder. `tide.WriteUpstream` refuses to write an Authorization or X-Api-Key header that no variable masks.
|
||||
|
||||
### Replaying offline
|
||||
|
||||
In a Go test, load the sidecar with `tide.LoadUpstream`, build the fake with `tide.NewUpstreamFake` and hand it to the code under test through `fetchguard.WithTransport` (see [Outbound HTTP](outbound-http.md#testing-outbound-calls)). The fake answers each request from the next recorded exchange without dialing and asserts what the port sent: method, scheme, host, path, query (in any order), the compared headers with placeholders expanded from the variables store, and the body (JSON semantically, multipart part by part, base64 payloads by hash). `tide.UpstreamFake.Verify` then fails on any mismatch, any extra request and any recorded exchange the port never sent.
|
||||
|
||||
Reference in New Issue
Block a user