testinghq 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- testinghq-1.0.0/CHANGELOG.md +315 -0
- testinghq-1.0.0/CONTRIBUTING.md +36 -0
- testinghq-1.0.0/LICENSE +21 -0
- testinghq-1.0.0/MANIFEST.in +18 -0
- testinghq-1.0.0/PKG-INFO +266 -0
- testinghq-1.0.0/README.md +250 -0
- testinghq-1.0.0/docs/CONFIG.md +164 -0
- testinghq-1.0.0/docs/DOGFOOD.md +131 -0
- testinghq-1.0.0/docs/FORMATS.md +106 -0
- testinghq-1.0.0/docs/README.md +42 -0
- testinghq-1.0.0/docs/ROADMAP.md +152 -0
- testinghq-1.0.0/docs/SECURITY.md +96 -0
- testinghq-1.0.0/docs/decisions/0001-barrage-pacing.md +80 -0
- testinghq-1.0.0/docs/decisions/0002-python-floor-3-10.md +69 -0
- testinghq-1.0.0/docs/decisions/0003-what-the-suite-protects.md +49 -0
- testinghq-1.0.0/examples/README.md +80 -0
- testinghq-1.0.0/examples/demo.py +88 -0
- testinghq-1.0.0/examples/pipeline_demo.py +389 -0
- testinghq-1.0.0/examples/readback/chatwoot.toml +86 -0
- testinghq-1.0.0/examples/readback/freshdesk.toml +98 -0
- testinghq-1.0.0/examples/readback/generic-rest.toml +141 -0
- testinghq-1.0.0/examples/readback/handlehq.toml +100 -0
- testinghq-1.0.0/examples/readback/mail-sink.toml +112 -0
- testinghq-1.0.0/examples/readback/mailpit.toml +69 -0
- testinghq-1.0.0/examples/readback/zendesk.toml +106 -0
- testinghq-1.0.0/examples/target.example.toml +158 -0
- testinghq-1.0.0/pyproject.toml +46 -0
- testinghq-1.0.0/setup.cfg +4 -0
- testinghq-1.0.0/testinghq/__init__.py +4 -0
- testinghq-1.0.0/testinghq/barrage/__init__.py +18 -0
- testinghq-1.0.0/testinghq/barrage/executor.py +134 -0
- testinghq-1.0.0/testinghq/barrage/fire.py +549 -0
- testinghq-1.0.0/testinghq/barrage/payloads.py +49 -0
- testinghq-1.0.0/testinghq/barrage/report.py +650 -0
- testinghq-1.0.0/testinghq/barrage/runner.py +588 -0
- testinghq-1.0.0/testinghq/blast/__init__.py +1 -0
- testinghq-1.0.0/testinghq/blast/attachments.py +235 -0
- testinghq-1.0.0/testinghq/blast/catalog.py +418 -0
- testinghq-1.0.0/testinghq/blast/corpus.py +204 -0
- testinghq-1.0.0/testinghq/blast/corrupt.py +678 -0
- testinghq-1.0.0/testinghq/blast/generate.py +313 -0
- testinghq-1.0.0/testinghq/blast/payload.py +222 -0
- testinghq-1.0.0/testinghq/blast/serialize.py +124 -0
- testinghq-1.0.0/testinghq/cli/__init__.py +32 -0
- testinghq-1.0.0/testinghq/cli/__main__.py +15 -0
- testinghq-1.0.0/testinghq/cli/barrage.py +293 -0
- testinghq-1.0.0/testinghq/cli/blast.py +516 -0
- testinghq-1.0.0/testinghq/cli/common.py +275 -0
- testinghq-1.0.0/testinghq/cli/main.py +98 -0
- testinghq-1.0.0/testinghq/cli/pipeline.py +715 -0
- testinghq-1.0.0/testinghq/cli/sigcheck.py +115 -0
- testinghq-1.0.0/testinghq/cli/utility.py +145 -0
- testinghq-1.0.0/testinghq/compare/__init__.py +32 -0
- testinghq-1.0.0/testinghq/compare/load.py +356 -0
- testinghq-1.0.0/testinghq/compare/runs.py +503 -0
- testinghq-1.0.0/testinghq/config_doc.py +392 -0
- testinghq-1.0.0/testinghq/core/__init__.py +1 -0
- testinghq-1.0.0/testinghq/core/config.py +354 -0
- testinghq-1.0.0/testinghq/core/config_schema.py +682 -0
- testinghq-1.0.0/testinghq/core/exit_codes.py +90 -0
- testinghq-1.0.0/testinghq/core/formats.py +500 -0
- testinghq-1.0.0/testinghq/core/guardrails.py +336 -0
- testinghq-1.0.0/testinghq/core/ratelimit.py +136 -0
- testinghq-1.0.0/testinghq/core/report.py +533 -0
- testinghq-1.0.0/testinghq/core/targets.py +109 -0
- testinghq-1.0.0/testinghq/core/transport.py +388 -0
- testinghq-1.0.0/testinghq/pipeline/__init__.py +166 -0
- testinghq-1.0.0/testinghq/pipeline/adapters.py +1607 -0
- testinghq-1.0.0/testinghq/pipeline/common.py +903 -0
- testinghq-1.0.0/testinghq/pipeline/expectations.py +949 -0
- testinghq-1.0.0/testinghq/pipeline/fixtures/steady_intents.json +264 -0
- testinghq-1.0.0/testinghq/pipeline/ledger.py +844 -0
- testinghq-1.0.0/testinghq/pipeline/loop.py +1106 -0
- testinghq-1.0.0/testinghq/pipeline/messages.py +298 -0
- testinghq-1.0.0/testinghq/pipeline/readback.py +523 -0
- testinghq-1.0.0/testinghq/pipeline/redeliver.py +1228 -0
- testinghq-1.0.0/testinghq/pipeline/sigcheck.py +999 -0
- testinghq-1.0.0/testinghq/pipeline/steady.py +1257 -0
- testinghq-1.0.0/testinghq/pipeline/toolconfig.py +86 -0
- testinghq-1.0.0/testinghq/pipeline/verify.py +1063 -0
- testinghq-1.0.0/testinghq/report_html/__init__.py +521 -0
- testinghq-1.0.0/testinghq/report_html/render.js +178 -0
- testinghq-1.0.0/testinghq/report_html/template.html +129 -0
- testinghq-1.0.0/testinghq/reporting.py +1088 -0
- testinghq-1.0.0/testinghq.egg-info/PKG-INFO +266 -0
- testinghq-1.0.0/testinghq.egg-info/SOURCES.txt +88 -0
- testinghq-1.0.0/testinghq.egg-info/dependency_links.txt +1 -0
- testinghq-1.0.0/testinghq.egg-info/entry_points.txt +2 -0
- testinghq-1.0.0/testinghq.egg-info/requires.txt +8 -0
- testinghq-1.0.0/testinghq.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to TestingHQ are recorded here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project uses [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
From 1.0.0 on, a change to a flag, an exit code, a config key or the
|
|
8
|
+
`report --json` shape is a breaking change and needs a major version.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [1.0.0] - 2026-10-15
|
|
13
|
+
|
|
14
|
+
The first published release.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
**Tools**
|
|
19
|
+
|
|
20
|
+
- `blast generate|fire|replay`: seeded inbound-email payloads, from clean to
|
|
21
|
+
degenerate, fired at an endpoint you control.
|
|
22
|
+
- `barrage fire|replay`: load at a steady or ramped rate, open loop or closed
|
|
23
|
+
loop, with a knee detector.
|
|
24
|
+
- `verify fire|check`: check what a pipeline actually stored, not what it
|
|
25
|
+
answered. A 200 is not evidence a message was parsed. `verify check` reads
|
|
26
|
+
back a run that already happened and refuses a blast artifact, which has no
|
|
27
|
+
tags.
|
|
28
|
+
- `ledger fire`: account for every payload sent against every record produced:
|
|
29
|
+
missing, duplicated, misparsed and stray records.
|
|
30
|
+
- `redeliver fire`: duplicate, slow-retry, reply-first and references delivery
|
|
31
|
+
scenarios, and whether the pipeline handles each.
|
|
32
|
+
- `loop fire`: machine mail and auto-reply loop detection.
|
|
33
|
+
- `steady fire`: label stability under meaning-preserving rewrites of the same
|
|
34
|
+
message.
|
|
35
|
+
- `sigcheck fire`: webhook signature enforcement for Mailgun, SendGrid and
|
|
36
|
+
Postmark basic auth. Up to six variants (`valid`, `wrong-secret`,
|
|
37
|
+
`unsigned`, `stale-timestamp`, `tampered-body`, `replayed-token`); a variant
|
|
38
|
+
the scheme has nothing to vary is not sent. The secret is an `env:NAME`
|
|
39
|
+
reference and a literal secret is refused. Dry run by default. The dry run
|
|
40
|
+
refuses a target whose format the scheme cannot sign, as `--send` does.
|
|
41
|
+
- `compare`: diff two blast runs payload by payload, or two barrage runs on
|
|
42
|
+
load metrics (see Compare below).
|
|
43
|
+
- `report`: read any tool's artifact and summarise it as text, as JSON in one
|
|
44
|
+
stable shape, or with `--html OUT` as a single self-contained HTML page
|
|
45
|
+
(verdict, counts, findings, records, Barrage charts as inline SVG, config,
|
|
46
|
+
exit code; light and dark; no network).
|
|
47
|
+
- `config validate`: load a config with the real loaders and print what they
|
|
48
|
+
resolved, including each target's `format`, `public` and public-target
|
|
49
|
+
resolution. Sends nothing and never prints a header value.
|
|
50
|
+
|
|
51
|
+
**Compare**
|
|
52
|
+
|
|
53
|
+
- Two barrage runs are diffed on error rate, hold-stage p99 latency and
|
|
54
|
+
achieved rps. A candidate regressed when the error or unsent rate rises by
|
|
55
|
+
more than 1 point, hold p99 rises by more than 20% and 5ms, or achieved rate
|
|
56
|
+
falls by more than 10% (judged only when mode and rate match).
|
|
57
|
+
- It refuses a dry-run artifact, an artifact from another tool and a pair
|
|
58
|
+
that mixes a blast run with a barrage run. It says "nothing to compare" when
|
|
59
|
+
no blast records align.
|
|
60
|
+
- `--fail-on-regression` exits 3 on a regression. Without it the exit is 0.
|
|
61
|
+
The artifact records `fail_on_regression` and the exit code.
|
|
62
|
+
|
|
63
|
+
**Report**
|
|
64
|
+
|
|
65
|
+
- `report --json` always carries `schema_version` and `aborted`, in one fixed
|
|
66
|
+
key set for every tool.
|
|
67
|
+
- It reads the artifacts of every tool, including steady, sigcheck and
|
|
68
|
+
compare.
|
|
69
|
+
- `tool` is `verify` for both `verify fire` and `verify check`; the text report
|
|
70
|
+
shows which.
|
|
71
|
+
- A blast dry-run artifact reads as DRY RUN with exit code 2, and nothing in it
|
|
72
|
+
counts as sent.
|
|
73
|
+
- A compare artifact reports the exit code the command gave, which depends on
|
|
74
|
+
`--fail-on-regression`.
|
|
75
|
+
|
|
76
|
+
**Sending**
|
|
77
|
+
|
|
78
|
+
- `--allow-public-target` on every send command. Barrage at a public host also
|
|
79
|
+
needs `public = true` on the target's config table.
|
|
80
|
+
- Target hosts are resolved: a name that resolves only to private, loopback or
|
|
81
|
+
link-local addresses counts as private.
|
|
82
|
+
- `[targets.<name>].format` selects the wire format, and every send path uses
|
|
83
|
+
it: `sendgrid`, `sendgrid-raw` (raw MIME in the `email` field), `mailgun`,
|
|
84
|
+
`postmark` and `mime`. `docs/FORMATS.md` documents each.
|
|
85
|
+
- The `sendgrid` format carries the documented fields it was missing: `dkim`,
|
|
86
|
+
`SPF`, `sender_ip`, `spam_score`, `spam_report`, `content-ids`, `cc`,
|
|
87
|
+
`charsets.text` and `attachment-info[*].content-id`.
|
|
88
|
+
- A header hook: `transport.post` and `transport.build_request` accept
|
|
89
|
+
`extra_headers`, a callable over the finished body bytes.
|
|
90
|
+
|
|
91
|
+
**Blast**
|
|
92
|
+
|
|
93
|
+
- `--mix clean=20,messy=40,...`, `--catalog [names]` and
|
|
94
|
+
`--attachments none|seeded|adversarial` (default `seeded`).
|
|
95
|
+
- `blast fire --send --catalog` sends every case. The synthetic-address guard
|
|
96
|
+
skips a field that is deliberately empty, and refuses any other value that
|
|
97
|
+
holds no address.
|
|
98
|
+
- `--fail-on-body <regex>`: a 2xx whose body matches is a flag.
|
|
99
|
+
- Catalog cases `null_byte_in_body`, `rtl_override_in_filename`,
|
|
100
|
+
`missing_boundary_field`, `duplicate_form_field`, `malformed_envelope_json`
|
|
101
|
+
and `oversized_body_1mb`; Outlook and Apple Mail quote and forward shapes.
|
|
102
|
+
- Realistic clean mail: consistent names, topics and weekdays, signatures,
|
|
103
|
+
Gmail and Outlook HTML wrappers, threading headers on replies, `Received`,
|
|
104
|
+
`Return-Path` and `Authentication-Results` on every message, RFC 2047
|
|
105
|
+
encoded non-ASCII subjects in `headers`.
|
|
106
|
+
- Encoding sabotage puts real cp1252, latin-1 and shift_jis bytes on the wire
|
|
107
|
+
with a lying `charsets` entry, for `text` as well as subject and html.
|
|
108
|
+
|
|
109
|
+
**Barrage**
|
|
110
|
+
|
|
111
|
+
- Concurrent dispatch through a pool of `--concurrency` worker threads, capped
|
|
112
|
+
at 64 unless `--allow-high-rate` is passed. A pool too small for the schedule
|
|
113
|
+
shows as achieved throughput falling short, and the run says how many
|
|
114
|
+
requests waited for a free worker and for how long.
|
|
115
|
+
- A real closed loop: N workers each send, wait, and take the next token. A
|
|
116
|
+
shortfall that `--concurrency` explains (workers / median latency is below
|
|
117
|
+
the planned rate) is not reported as an endpoint knee; the summary carries
|
|
118
|
+
`concurrency_limited` and says to raise `--concurrency`.
|
|
119
|
+
- One keep-alive connection per worker (`--no-keepalive` opens one per
|
|
120
|
+
request), with connection setup time reported separately.
|
|
121
|
+
- `--timeout` (default 10s), `--grace` (default 10s) and
|
|
122
|
+
`--max-queue-seconds` (default 5).
|
|
123
|
+
- Per-request `records` in the artifact, so `compare` can diff two load runs.
|
|
124
|
+
- Report: 429 counted and named "shed", a status-class histogram per bucket,
|
|
125
|
+
p50, p90 and p99 per bucket, service and offered latency series, and
|
|
126
|
+
percentiles overall and for the hold stage.
|
|
127
|
+
|
|
128
|
+
**Readback**
|
|
129
|
+
|
|
130
|
+
- Retry with backoff and jitter on 429, 5xx, timeouts and connection errors,
|
|
131
|
+
honouring `Retry-After` (`retry_max`, default 5), and a readback rate cap
|
|
132
|
+
(`--readback-rate`, default 4/s).
|
|
133
|
+
- A preflight request before anything is sent: refused credentials (401 or
|
|
134
|
+
403), a response that is not JSON, a field map that finds no identifier or a
|
|
135
|
+
list url that 404s is a refusal. A 404 on the tag lookup is not, because a
|
|
136
|
+
missing record answers 404; a url that 404s every lookup is READBACK FAILED.
|
|
137
|
+
- `query` templates, `method = "POST"` with a `body` template, and pagination
|
|
138
|
+
by `next_path` or `page_param`/`page_size_param` with `max_pages`.
|
|
139
|
+
- `*` wildcards in field paths, `false` to unmap a default field, and senders
|
|
140
|
+
stored as objects.
|
|
141
|
+
- Settling rule, set by `--quiet-window` (default 5s), `--max-wait` (default
|
|
142
|
+
60s) and `--poll-interval`. The readback is settled once the found tags,
|
|
143
|
+
their record counts and the checked field values have held still for
|
|
144
|
+
`--quiet-window`, counted from the start of the first of two identical
|
|
145
|
+
complete polls. A tag still missing then is lost. Holding still is also
|
|
146
|
+
what catches a duplicate that lands after the first sighting.
|
|
147
|
+
- UNSETTLED (exit 3) only when `--max-wait` runs out while the store is still
|
|
148
|
+
changing, or before every tag could be polled twice. The headline names which
|
|
149
|
+
and gives the numbers, with the `--max-wait` or `--readback-rate` that would
|
|
150
|
+
fit. Findings already proven stay listed and counted.
|
|
151
|
+
- Every readback tool's dry run warns when the plan cannot settle within a
|
|
152
|
+
given `--max-wait`: about 2 x tags / `--readback-rate` plus `--quiet-window`.
|
|
153
|
+
- `--max-wait` not given grows with the plan: the larger of 60s and that settle
|
|
154
|
+
time plus 25%, so a plan above about 110 tags at the default
|
|
155
|
+
`--readback-rate` still settles. A value given is used as is. The dry run
|
|
156
|
+
prints the max-wait a real run would use, and the artifact records it with
|
|
157
|
+
whether it was the default or given.
|
|
158
|
+
- `--max-wait`, `--poll-interval` and `--quiet-window` are checked by the
|
|
159
|
+
parser, before anything is sent: the first two must be above zero, the last
|
|
160
|
+
zero or more.
|
|
161
|
+
- Every `--rate` and `--readback-rate` must be a positive number, refused at
|
|
162
|
+
the parser.
|
|
163
|
+
- READBACK FAILED (exit 3) when every lookup was a 404. The readback url is
|
|
164
|
+
almost certainly wrong, so no message is reported as lost: ledger lists the
|
|
165
|
+
tags under `not_checked`, verify and redeliver say "not checked: every
|
|
166
|
+
lookup was 404", and `report` shows one readback-failed finding.
|
|
167
|
+
- Readback fields `category` and `priority`, so a tool can read a classifier's
|
|
168
|
+
label.
|
|
169
|
+
- Recipes for Zendesk, Freshdesk, a generic REST API, an outbound mail sink,
|
|
170
|
+
Mailpit, Chatwoot and a HandleHQ deployment in `examples/readback/`.
|
|
171
|
+
- Per-run default tag prefix `hq-<yymmddHHMM>-<4 hex>`; `--tag-prefix`
|
|
172
|
+
overrides it and the artifact records it.
|
|
173
|
+
- Message-IDs are unique per run, so a pipeline that drops a repeated
|
|
174
|
+
Message-ID does not drop a second run. Verify, ledger, redeliver and steady
|
|
175
|
+
derive each from the message's tag; sigcheck draws one per run and every
|
|
176
|
+
variant shares it. One explicit `--tag-prefix` gives the same Message-IDs on
|
|
177
|
+
every run.
|
|
178
|
+
|
|
179
|
+
**Checks**
|
|
180
|
+
|
|
181
|
+
- `--subject-match exact|contains|suffix` (default `contains`) and
|
|
182
|
+
`--attachment-match exact|loose`, honoured by verify, ledger and redeliver
|
|
183
|
+
and recorded in the artifact.
|
|
184
|
+
- `--expect-route` honoured by verify, ledger and redeliver.
|
|
185
|
+
- `verify check` re-checks with the match modes the artifact recorded and says
|
|
186
|
+
where each came from. A flag overrides one; `--no-expect-route` and
|
|
187
|
+
`--no-body-exact` turn those two off.
|
|
188
|
+
- `[loop].ticket_policy` and `[steady].intents_path` config keys.
|
|
189
|
+
- A key or top-level table the schema does not declare is refused (exit 1)
|
|
190
|
+
before anything is sent, naming the file, the table, the key and the closest
|
|
191
|
+
known key. `config validate` reports the same error and checks the key names
|
|
192
|
+
of `[loop]`, `[steady]` and `[sigcheck]`. The names inside `[readback.headers]`
|
|
193
|
+
and `[readback.fields]` stay open, and a custom adapter's table may carry its
|
|
194
|
+
own keys.
|
|
195
|
+
|
|
196
|
+
**Artifacts and project**
|
|
197
|
+
|
|
198
|
+
- Every artifact carries `schema_version: 1` and a top-level `tool`. Readers
|
|
199
|
+
treat a missing `schema_version` as 0 and refuse a newer one.
|
|
200
|
+
- `docs/CONFIG.md`, generated from `testinghq/core/config_schema.py`, with a
|
|
201
|
+
test that fails when the two drift.
|
|
202
|
+
- `docs/README.md`, an index of the docs, and `docs/DOGFOOD.md`, how to run
|
|
203
|
+
the suite against a real deployment.
|
|
204
|
+
- `examples/pipeline_demo.py` runs verify, ledger, redeliver, loop and steady
|
|
205
|
+
over real sockets against a pipeline that imports nothing from TestingHQ and
|
|
206
|
+
owns its machine-mail check, tag regex, Message-ID dedup, threading and
|
|
207
|
+
classifier. Its defects are on the write side and each run's exit code is
|
|
208
|
+
asserted.
|
|
209
|
+
- `CONTRIBUTING.md`, `ruff.toml` (pyflakes rules), a lint job in CI, and a
|
|
210
|
+
`report-html-tests` job that runs the render.js tests against the installed
|
|
211
|
+
wheel.
|
|
212
|
+
|
|
213
|
+
### Changed
|
|
214
|
+
|
|
215
|
+
- **One exit-code convention across every tool**: 0 ran and yes, 1 refused, 2
|
|
216
|
+
dry run, 3 ran and found something. A usage error exits 1 rather than
|
|
217
|
+
argparse's 2. `blast fire --send` exits 3 when anything is flagged.
|
|
218
|
+
- **`compare` shares the convention**: a usage error is 1 and, with
|
|
219
|
+
`--fail-on-regression`, a regression is 3.
|
|
220
|
+
- **Ctrl-C** in a long-running command writes what it has (when `--out` was
|
|
221
|
+
given) with `aborted: true` and exits 3.
|
|
222
|
+
- **Blast verdict**: a 5xx or transport failure in any category is a flag. A
|
|
223
|
+
4xx on malformed input is recorded and not flagged. Transport failures are
|
|
224
|
+
reported by reason (refused, DNS, timeout, TLS, other).
|
|
225
|
+
- **`headers` stay in sync** with `subject`, `to` and `from` after every
|
|
226
|
+
mutation and catalog case.
|
|
227
|
+
- **The default Blast corpus contains attachments** (`--attachments seeded`).
|
|
228
|
+
- **Barrage** ends at `duration + grace` whatever the target does: in-flight
|
|
229
|
+
requests past the deadline are `abandoned`, queued ones `not_dispatched`,
|
|
230
|
+
and requests that waited past `--max-queue-seconds` are `dropped_backlog`.
|
|
231
|
+
Achieved throughput is measured over the real dispatch span. The knee is the
|
|
232
|
+
first of two buckets in a row that shed (error rate of 5% or more), send over
|
|
233
|
+
20% fewer requests than planned, or run at over twice the hold-stage median
|
|
234
|
+
latency. In a closed loop, fewer requests than planned count only when the
|
|
235
|
+
workers could have sent more.
|
|
236
|
+
- **Routing is not checked unless `--expect-route` is given**; it reports
|
|
237
|
+
SKIPPED with "no expected route".
|
|
238
|
+
- **Subjects match by containment by default**, so a stored
|
|
239
|
+
"[Ticket #123] Re: ..." passes. An empty expected body is SKIPPED. HTML
|
|
240
|
+
bodies are compared after tag stripping. "Last, First <addr>" senders parse.
|
|
241
|
+
- **ledger** reports UNVERIFIED STRAYS when the adapter cannot enumerate,
|
|
242
|
+
instead of refusing after sending. "Extra" means records carrying this run's
|
|
243
|
+
tag family that this run did not send; other runs' records are listed
|
|
244
|
+
separately and untagged records are not strays. The `extra` list is capped
|
|
245
|
+
at 200 with a count.
|
|
246
|
+
- **`--max-wait` is one budget for the whole readback**: a request or retry
|
|
247
|
+
that would run past it ends the poll.
|
|
248
|
+
- **A spec adapter** (`--readback module:attr`) may be a class (instantiated),
|
|
249
|
+
an object with `fetch(probe)`, a one-argument fetch function, or a factory
|
|
250
|
+
taking nothing or `config`. An import error is a refusal before anything is
|
|
251
|
+
sent. The class or factory gets the table with the adapter's own keys as
|
|
252
|
+
written (an `env:NAME` value in them arrives unresolved) plus the built-in
|
|
253
|
+
keys; header names are included and header values are not.
|
|
254
|
+
- **redeliver** slow-retry sends every original, waits once, then sends every
|
|
255
|
+
retry; the references scenario checks the full `References` ancestry. Every
|
|
256
|
+
Message-ID, In-Reply-To and References id it writes is in angle brackets.
|
|
257
|
+
- **loop** enumerates the outbound sink, so an auto-reply it cannot attribute
|
|
258
|
+
is a finding ("outbound, unattributed") rather than "no reply". The loop
|
|
259
|
+
bait is an auto-reply from the customer's address to the pipeline's real
|
|
260
|
+
outbound Message-ID, falling back to a made-up parent when no acknowledgement
|
|
261
|
+
arrives within `--ack-wait`. `--reply-address` takes precedence over the
|
|
262
|
+
config file.
|
|
263
|
+
- **steady** headline is per-family majority dissent, with the pairwise flip
|
|
264
|
+
rate secondary (it gates the exit code only with `--max-flip-rate`). Cross-language pairs are
|
|
265
|
+
reported separately and every report has a sample-size line. It refuses when
|
|
266
|
+
every family would read the same constant label.
|
|
267
|
+
- The CLI is a package, one module per command. The command surface is
|
|
268
|
+
unchanged.
|
|
269
|
+
- `project.license` is the SPDX string `"MIT"`.
|
|
270
|
+
- The sdist ships the package, the docs and the examples, not `tests/`.
|
|
271
|
+
- The suite's meta tests (README wording, CI YAML, docstrings, file absence)
|
|
272
|
+
are cut to the ones that guard behaviour.
|
|
273
|
+
`docs/decisions/0003-what-the-suite-protects.md` records what each removed
|
|
274
|
+
check had encoded.
|
|
275
|
+
|
|
276
|
+
### Fixed
|
|
277
|
+
|
|
278
|
+
- **`--allow-public-readback` wins over `allow_public_hosts = false`** in the
|
|
279
|
+
`[readback]` table of the config file.
|
|
280
|
+
- `steady` runs from an installed wheel: its intent fixture is package data.
|
|
281
|
+
- A negative `--count` is a refusal, not a traceback.
|
|
282
|
+
- The mailbox readback adapter tolerates a partially written trailing line and
|
|
283
|
+
parses the file once per poll instead of once per tag per poll.
|
|
284
|
+
- A readback list under the wrong `items_key` says so by name.
|
|
285
|
+
- A relative mailbox path resolves against the config file's directory, not
|
|
286
|
+
the working directory.
|
|
287
|
+
- Undefined names (`Any` in `core/config.py`, `FormPart` in `core/formats.py`),
|
|
288
|
+
the `record_id_for` export that did not exist, and unused imports.
|
|
289
|
+
|
|
290
|
+
### Removed
|
|
291
|
+
|
|
292
|
+
- **The web UI** (`web/`), which exposed only Blast. Its rendering lives
|
|
293
|
+
on as `report --html`.
|
|
294
|
+
- **The marketing page** (`site/`).
|
|
295
|
+
- `--expect-route` on `loop` and `steady`, which never honoured it.
|
|
296
|
+
- `testinghq/pipeline/corpus.py`, a duplicate of code in `verify.py`.
|
|
297
|
+
|
|
298
|
+
### Security
|
|
299
|
+
|
|
300
|
+
- Public targets require explicit consent: `--allow-public-target`, plus
|
|
301
|
+
`public = true` for Barrage. Synthetic reserved-domain addresses, the
|
|
302
|
+
dry-run default and the configured-target allow-list are unchanged.
|
|
303
|
+
- Readback URLs on a public host are refused unless `--allow-public-readback`
|
|
304
|
+
is passed.
|
|
305
|
+
- `[readback.headers]` values must be `env:NAME` references, checked before
|
|
306
|
+
anything is sent, and an artifact records header names, never values.
|
|
307
|
+
- Any artifact written at a public target records `allow_public_target: true`
|
|
308
|
+
under `config`.
|
|
309
|
+
- The `report --html` page carries a Content-Security-Policy that allows only
|
|
310
|
+
its own inline script and style, and escapes every value from the artifact.
|
|
311
|
+
- The CI, e2e and security workflows run with a read-only token and have job
|
|
312
|
+
timeouts, and cancel superseded pull request runs.
|
|
313
|
+
|
|
314
|
+
[Unreleased]: https://github.com/saifullah4khan/TestingHQ/compare/v1.0.0...HEAD
|
|
315
|
+
[1.0.0]: https://github.com/saifullah4khan/TestingHQ/releases/tag/v1.0.0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Set up
|
|
4
|
+
|
|
5
|
+
git clone https://github.com/saifullah4khan/TestingHQ.git
|
|
6
|
+
cd TestingHQ
|
|
7
|
+
python -m venv .venv && . .venv/bin/activate
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
|
|
10
|
+
Python 3.10 is the floor. Runtime code uses the standard library only (plus
|
|
11
|
+
`tomli` below 3.11); a new runtime dependency needs a strong case.
|
|
12
|
+
|
|
13
|
+
## Check
|
|
14
|
+
|
|
15
|
+
pytest -q -m "not allow_network" # hermetic suite, what CI's tests job runs
|
|
16
|
+
pytest -q -m allow_network # real loopback sockets, what e2e runs
|
|
17
|
+
ruff check . # pyflakes rules, see ruff.toml
|
|
18
|
+
node --test tests/report_html/*.test.cjs
|
|
19
|
+
|
|
20
|
+
The suite blocks outbound sockets by default. A test that needs loopback is
|
|
21
|
+
marked `@pytest.mark.allow_network` and lives in `tests/e2e/` or
|
|
22
|
+
`tests/integration/`; collection fails anywhere else.
|
|
23
|
+
|
|
24
|
+
Test behaviour through public functions or the CLI. Do not add tests that
|
|
25
|
+
assert README wording, CI YAML, docstrings or that a file is absent; see
|
|
26
|
+
`docs/decisions/0003-what-the-suite-protects.md`.
|
|
27
|
+
|
|
28
|
+
## Commits and pull requests
|
|
29
|
+
|
|
30
|
+
- Lowercase `type: subject` (feat, fix, refactor, test, docs, chore, ci),
|
|
31
|
+
subject under 72 characters, and a body that says why.
|
|
32
|
+
- No em-dashes anywhere, in code, comments, docs or commit messages.
|
|
33
|
+
- Every behaviour change ships with a test that fails before and passes after.
|
|
34
|
+
- Update `CHANGELOG.md` under `[Unreleased]`. A change to a flag, exit code,
|
|
35
|
+
config key or the `report --json` shape is breaking.
|
|
36
|
+
- Both required checks, `tests` and `end-to-end`, must be green.
|
testinghq-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saifullah Khan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# What goes in the sdist. The wheel carries only the importable package; the
|
|
2
|
+
# sdist adds what a reader of the source needs: the changelog, the docs and
|
|
3
|
+
# the example configs.
|
|
4
|
+
#
|
|
5
|
+
# The test suite is not shipped. It needs the repository (CI workflows, git,
|
|
6
|
+
# the e2e harness), so from an extracted sdist it could not pass, and a suite
|
|
7
|
+
# that fails out of the box is worse than none. Clone the repository to run it.
|
|
8
|
+
|
|
9
|
+
include CHANGELOG.md
|
|
10
|
+
include CONTRIBUTING.md
|
|
11
|
+
include LICENSE
|
|
12
|
+
include README.md
|
|
13
|
+
|
|
14
|
+
recursive-include docs *.md
|
|
15
|
+
recursive-include examples *.toml *.py *.md
|
|
16
|
+
|
|
17
|
+
prune tests
|
|
18
|
+
prune .github
|
testinghq-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: testinghq
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Fuzz, load-test and verify your inbound email webhook. Catches lost, duplicated, mis-threaded and looped mail that a 200 OK hides.
|
|
5
|
+
Author-email: Saifullah Khan <saifullah4khan@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
14
|
+
Requires-Dist: setuptools>=77; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# TestingHQ
|
|
18
|
+
|
|
19
|
+
Fuzz, load-test and verify your inbound email webhook. Catches lost, duplicated,
|
|
20
|
+
mis-threaded and looped mail that a 200 OK hides.
|
|
21
|
+
|
|
22
|
+
TestingHQ posts provider-shaped inbound email (SendGrid, Mailgun, Postmark or raw
|
|
23
|
+
MIME) at a webhook you run, then asks your system what it actually made of each
|
|
24
|
+
message.
|
|
25
|
+
|
|
26
|
+
## What it catches
|
|
27
|
+
|
|
28
|
+
- An email your endpoint answered 200 for and never turned into a ticket.
|
|
29
|
+
- Two tickets for one email: a provider retry, a redelivery, a worker that ran twice.
|
|
30
|
+
- A ticket nobody sent, carrying a test tag.
|
|
31
|
+
- A mangled sender, a changed subject, a truncated body, a dropped attachment.
|
|
32
|
+
- A reply filed as a new conversation, including one that arrives before its original.
|
|
33
|
+
- An auto-reply to an out-of-office, a bounce or a mailing list: how a mail loop starts.
|
|
34
|
+
- A triage label that moves when a signature, a forward or a translation is added.
|
|
35
|
+
- A webhook that accepts unsigned, wrongly signed, stale or replayed requests.
|
|
36
|
+
- A parser that answers 500 to messy, multilingual or malformed mail, or 200 with an error.
|
|
37
|
+
- The request rate at which the endpoint starts slowing down or shedding load.
|
|
38
|
+
|
|
39
|
+
## Which tool
|
|
40
|
+
|
|
41
|
+
| Question | Tool | Answer | Exit 3 means |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| Does my parser survive messy mail? | `blast` | a seeded corpus fired at the webhook, every bad answer flagged | a payload was flagged |
|
|
44
|
+
| How much load before it slows or sheds? | `barrage` | throughput, latency percentiles and the knee | the run was interrupted |
|
|
45
|
+
| Did it store what was sent? | `verify` | six checks per message, read back from your system | a check failed, or the readback never settled |
|
|
46
|
+
| Did we lose or duplicate anything? | `ledger` | missing, duplicated, misparsed and extra, counted | anything but BALANCED |
|
|
47
|
+
| Are retries and out-of-order replies safe? | `redeliver` | four provider misbehaviours, one ticket and one thread each | a scenario failed |
|
|
48
|
+
| Will it answer an auto-reply? | `loop` | twelve kinds of machine mail and a loop bait | a ticket or a reply for machine mail |
|
|
49
|
+
| Is the triage label stable? | `steady` | label dissent across meaning-preserving rewrites | dissent above the gate |
|
|
50
|
+
| Does it check the webhook signature? | `sigcheck` | six signed and mis-signed variants | it accepted one it should refuse |
|
|
51
|
+
| Did my change make it worse? | `compare` | regressions, fixes and changes between two runs | a regression, with `--fail-on-regression` |
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install testinghq
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Python 3.10 or newer. Standard library only at run time, plus `tomli` on 3.10.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
cat > target.toml <<'EOF'
|
|
65
|
+
[targets.local]
|
|
66
|
+
url = "http://127.0.0.1:8000/inbound" # your webhook
|
|
67
|
+
[targets.mailgun] # sigcheck --scheme mailgun needs format = "mailgun"
|
|
68
|
+
url = "http://127.0.0.1:8000/inbound"
|
|
69
|
+
format = "mailgun"
|
|
70
|
+
|
|
71
|
+
[readback]
|
|
72
|
+
kind = "http"
|
|
73
|
+
url = "http://127.0.0.1:8000/tickets" # answers GET ?tag=<tag> with the records it made
|
|
74
|
+
EOF
|
|
75
|
+
testinghq config validate target.toml
|
|
76
|
+
testinghq ledger fire --target local --count 50
|
|
77
|
+
testinghq ledger fire --target local --count 50 --send --out ledger.json
|
|
78
|
+
testinghq report ledger.json --html report.html
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Every command is a dry run until you pass `--send`. Records are read with the
|
|
82
|
+
field names in [docs/CONFIG.md](docs/CONFIG.md) unless `[readback.fields]` maps them; an unknown config key is refused (exit 1) with the closest known one.
|
|
83
|
+
A target's `format` picks the wire format: `sendgrid` (default), `sendgrid-raw`, `mailgun`, `postmark` or `mime`; see [docs/FORMATS.md](docs/FORMATS.md).
|
|
84
|
+
|
|
85
|
+
## What a finding looks like
|
|
86
|
+
|
|
87
|
+
`ledger` against a pipeline whose worker writes every fifth ticket twice. Every
|
|
88
|
+
POST in the run was answered 200. From `python examples/pipeline_demo.py`:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
ledger fire: explicit --send flag set
|
|
92
|
+
ledger: UNACCOUNTED (12 message(s) sent, seed=0, tag prefix 'hq-2610142218-d0f4')
|
|
93
|
+
|
|
94
|
+
sent: 12
|
|
95
|
+
produced: 14
|
|
96
|
+
exactly once: 10
|
|
97
|
+
missing: 0
|
|
98
|
+
duplicated: 2
|
|
99
|
+
misparsed: 0
|
|
100
|
+
readback: 2.59s over 4 poll(s), settled
|
|
101
|
+
extra: 0
|
|
102
|
+
|
|
103
|
+
missing: none
|
|
104
|
+
|
|
105
|
+
duplicated (2):
|
|
106
|
+
hq-2610142218-d0f4-0-0004: 2 tickets ['T00005', 'T00006']
|
|
107
|
+
hq-2610142218-d0f4-0-0009: 2 tickets ['T00011', 'T00012']
|
|
108
|
+
|
|
109
|
+
misparsed: none
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Exit codes
|
|
113
|
+
|
|
114
|
+
| Code | Meaning | A script should |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| 0 | ran, the answer was yes | carry on |
|
|
117
|
+
| 1 | refused: guardrail, bad config or bad usage; nothing was sent | fix the command |
|
|
118
|
+
| 2 | dry run: no `--send`, nothing was sent | carry on |
|
|
119
|
+
| 3 | ran, the answer was no | read the report |
|
|
120
|
+
|
|
121
|
+
Every tool uses the same four codes. `report` exits 0 whenever it could read the artifact.
|
|
122
|
+
|
|
123
|
+
## The tools
|
|
124
|
+
|
|
125
|
+
**Blast.** Fires a seeded corpus (clean, messy, multilingual, malformed, degenerate) with one to three clean attachments on about a third of payloads (`--attachments none|seeded|adversarial`).
|
|
126
|
+
`--mix clean=20,messy=40,...` sets the category percentages; `--catalog` fires the named edge cases instead, all of them or a comma-separated list.
|
|
127
|
+
A 5xx or no response is flagged in every category, and a missing response says why (refused, DNS, timeout, TLS, other). `--fail-on-body REGEX` flags a 2xx whose body reports failure.
|
|
128
|
+
The same seed and options rebuild a byte-identical corpus, and `blast replay` re-fires it.
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
testinghq blast generate --count 100 --seed 1 --out corpus
|
|
132
|
+
testinghq blast fire --target local --send --catalog --out blast.json
|
|
133
|
+
testinghq blast replay blast.json --send
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Barrage.** Clean payloads at a controlled rate after a warmup ramp. `--mode open` (default) keeps arrivals on schedule whatever the target does; `--mode closed` runs `--concurrency` workers that each send the next request when the last completes.
|
|
137
|
+
It reports achieved against target throughput, p50/p90/p99 latency, errors over time with 429 counted as shed load, and the knee where the endpoint gives way.
|
|
138
|
+
The run ends `--grace` seconds (default 10) after `--duration`, and a request that waited longer than `--max-queue-seconds` (default 5) for a worker is dropped and counted. Each worker keeps one connection alive unless `--no-keepalive`.
|
|
139
|
+
Ctrl-C writes a partial artifact with `aborted: true` and exits 3. Rate, duration and concurrency are capped at 50/s, 300s and 64 unless `--allow-high-rate`.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
testinghq barrage fire --target local --rate 20 --duration 60 --send --out load.json
|
|
143
|
+
testinghq barrage fire --target local --mode closed --concurrency 8 --send
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Verify.** Fires clean tagged messages, reads back what the system stored, and checks that a record exists and that its sender, subject, body, attachments and route match.
|
|
147
|
+
Routing is checked only when you pass `--expect-route`; without it the check is skipped. `--subject-match contains` (the default) accepts helpdesk prefixes like `[Ticket #123]`, and `--attachment-match loose` tolerates sanitised filenames.
|
|
148
|
+
A check that could not run says why (no record found, adapter cannot see field, nothing to check) and never counts as a pass.
|
|
149
|
+
`verify check` re-reads an earlier run without sending anything.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
testinghq verify fire --target local --send --expect-route support --out verify.json
|
|
153
|
+
testinghq verify check verify.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Ledger.** Sends N uniquely tagged messages and reconciles them: missing, duplicated, misparsed, and extra (a record with this run's tag family that this run never sent).
|
|
157
|
+
Each run draws its own tag prefix, `hq-<yymmddHHMM>-<4 hex>`, so runs never read each other's records; `--tag-prefix` fixes one.
|
|
158
|
+
BALANCED exits 0. A readback that cannot list every record reports UNVERIFIED STRAYS, and one still changing at `--max-wait` reports UNSETTLED; both exit 3.
|
|
159
|
+
Run it while Barrage holds the system under load to see what load loses.
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
testinghq ledger fire --target local --send --count 50
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Redeliver.** Four scenarios: the same Message-ID delivered twice; every message re-sent after one `--retry-after` wait (default 120s); two replies delivered before their original; a three-message thread whose last reply must carry the full References chain.
|
|
166
|
+
Duplicate tickets and threading are checked separately. `--scenario` picks one, and can be repeated.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
testinghq redeliver fire --target local --send --scenario slow-retry --retry-after 30
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Loop.** Sends machine mail a pipeline must not act on (Auto-Submitted, Precedence, out-of-office, a delivery-status bounce, noreply senders, list mail) and a loop bait: an ordinary message, then an autoresponder answering the pipeline's own acknowledgement of it.
|
|
173
|
+
A ticket for machine mail is a finding unless `--ticket-policy allowed`; a reply to machine mail, or to the bait, is always a finding.
|
|
174
|
+
Replies are read from `[loop.outbound]` in the config. Without it the reply check is NOT CHECKED, never passed, and outbound mail it cannot tie to a message is itself a finding.
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
testinghq loop fire --target local --send --ticket-policy allowed
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Steady.** Sends reviewed intents in English, Urdu and Roman Urdu, each with meaning-preserving edits: a signature, a forward wrapper, quoted history, reflowed whitespace, a reworded greeting.
|
|
181
|
+
It reads back the label (`--label-field category|route|priority`) and reports the share of variants that dissent from their family's majority, by transform and by language. Above `--max-dissent-rate` (default 0.1) it exits 3.
|
|
182
|
+
`--repeats N` measures answers to identical input separately. It measures stability, not correctness.
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
testinghq steady fire --target local --send --label-field category --repeats 3
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Sigcheck.** Sends one request per variant (valid, wrong-secret, unsigned, stale-timestamp, tampered-body, replayed-token) and reports which the endpoint accepted.
|
|
189
|
+
Schemes: `mailgun` (HMAC of timestamp and token, as form fields); `sendgrid` (ECDSA over timestamp and raw body, on the curve SendGrid documents: give the private key matching the endpoint's public key, and the dry run prints that public key); `postmark-basic` (Postmark does not sign; its protection is Basic credentials).
|
|
190
|
+
The secret is read only from the environment variable `env:NAME` names, and the artifact records the reference, never the value. For Mailgun, accepting a stale timestamp but refusing a replayed token follows Mailgun's own guidance and is informational.
|
|
191
|
+
Exit 3: the endpoint accepted what it should refuse, refused a valid request, or answered 5xx.
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
testinghq sigcheck fire --target mailgun --scheme mailgun --secret env:MAILGUN_SIGNING_KEY --send
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Compare.** Two Blast runs are lined up payload by payload, with regressions, fixes and changes reported apart. Two Barrage runs are compared on load: error and unsent rate, hold-stage p99, and achieved rate when mode and rate match.
|
|
198
|
+
A dry run on either side, or two runs with nothing in common, is refused.
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
testinghq compare --baseline before.json --candidate after.json --fail-on-regression
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Reports.** `testinghq report run.json` summarises any tool's artifact, and `testinghq report run.json --json` prints one shape for every tool. `testinghq report run.json --html report.html` writes a self-contained HTML report. `testinghq config validate` shows what the loaders resolved and never prints a header value.
|
|
205
|
+
|
|
206
|
+
## Readback
|
|
207
|
+
|
|
208
|
+
The pipeline tools ask your system what it made of each message, found by a tag
|
|
209
|
+
in an `X-TestingHQ-Tag` header and `[testinghq:<tag>]` in the body. Three kinds:
|
|
210
|
+
|
|
211
|
+
```toml
|
|
212
|
+
[readback] # a JSON API: GET url?tag=<tag>
|
|
213
|
+
kind = "http"
|
|
214
|
+
url = "http://127.0.0.1:8000/tickets"
|
|
215
|
+
query = "type:ticket tags:{tag}" # optional template, for search APIs
|
|
216
|
+
|
|
217
|
+
[readback.headers]
|
|
218
|
+
Authorization = "env:HQ_READBACK_TOKEN" # env: references only; a literal is refused
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```toml
|
|
222
|
+
[readback] # a JSON Lines file your pipeline writes
|
|
223
|
+
kind = "mailbox"
|
|
224
|
+
path = "./mail-sink.jsonl"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Anything else is an import path, `--readback mypkg.adapters:build`: a class, an
|
|
228
|
+
object with `fetch`, or a function taking one probe and returning records.
|
|
229
|
+
|
|
230
|
+
The readback settles once what it sees (which payloads are found, how many records each, the field
|
|
231
|
+
values the checks read) has held still for `--quiet-window` seconds (default 5); a payload still not
|
|
232
|
+
found then is lost. A store still changing when `--max-wait` (default 60, raised to fit a large run) runs out, or not polled twice by then, is UNSETTLED
|
|
233
|
+
(exit 3), not a loss, and anything already proven, such as a duplicate, is still listed. A run whose
|
|
234
|
+
every lookup was a 404 is READBACK FAILED (exit 3): the url is almost certainly wrong, so nothing is
|
|
235
|
+
reported as lost. For a slow or async pipeline, raise `--quiet-window` above its worst processing
|
|
236
|
+
latency. Lookups retry 429, 5xx and timeouts with backoff, honour `Retry-After`, and are paced to
|
|
237
|
+
`--readback-rate` (default 4 per second). A public readback host needs `--allow-public-readback`.
|
|
238
|
+
Recipes for Zendesk, Freshdesk, Mailpit, Chatwoot, a generic REST API and a mail sink are in
|
|
239
|
+
[examples/readback/](examples/readback/).
|
|
240
|
+
|
|
241
|
+
## Responsible use
|
|
242
|
+
|
|
243
|
+
TestingHQ is for endpoints you own or are authorised to test. It never sends
|
|
244
|
+
email: it POSTs webhook payloads, and every address in them is on a reserved
|
|
245
|
+
example domain.
|
|
246
|
+
|
|
247
|
+
- Dry run is the default; every run that sends needs `--send`. Only a target declared in `[targets.<name>]` can be fired at.
|
|
248
|
+
- A public host needs `--allow-public-target`; Barrage at a public host also needs `public = true` on the target.
|
|
249
|
+
- Every send is rate limited, and Barrage has hard ceilings.
|
|
250
|
+
|
|
251
|
+
See [docs/SECURITY.md](docs/SECURITY.md) for the full model and how to report a vulnerability.
|
|
252
|
+
|
|
253
|
+
## Documentation
|
|
254
|
+
|
|
255
|
+
- [docs/README.md](docs/README.md): index of everything below
|
|
256
|
+
- [docs/CONFIG.md](docs/CONFIG.md): every config key, generated from the code
|
|
257
|
+
- [docs/FORMATS.md](docs/FORMATS.md): the five wire formats, field by field
|
|
258
|
+
- [docs/SECURITY.md](docs/SECURITY.md): guardrails, public targets, threat model
|
|
259
|
+
- [docs/DOGFOOD.md](docs/DOGFOOD.md): running against a real staging deployment
|
|
260
|
+
- [docs/ROADMAP.md](docs/ROADMAP.md): goals, timeline and what comes after 1.0
|
|
261
|
+
- [examples/README.md](examples/README.md): the demo scripts and sample configs
|
|
262
|
+
- [CHANGELOG.md](CHANGELOG.md) and [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT. See [LICENSE](LICENSE).
|