From a8838afacecffe1a7974dbd496f4b4d7a6e14135 Mon Sep 17 00:00:00 2001 From: Mysttic Date: Fri, 31 Jul 2026 20:58:25 +0200 Subject: [PATCH] feat: ship a smoke-test script with every release Adds smoke-test.ps1 and smoke-test.sh to every release zip. Start the simulator, run one of them, and it exercises the whole documented contract - 25 checks - printing PASS or FAIL for each. It exits 1 when a check fails and 2, with an explanation, when the simulator is not reachable, so it also works as a CI gate. The container smoke test in CI now runs that same script instead of a few inline curl calls, so the tool that ships to users is exercised on every pull request and cannot rot unnoticed. Also documents how to drive the API from a browser: /healthz and /version open directly, and for the POST endpoint you open /healthz first and use fetch from the console - being on the simulator's own origin is what avoids CORS, since no CORS policy is configured. Fixes /version reporting the commit sha twice (1.0.3+.): the release pipeline appends it to InformationalVersion and the SDK appended it again. IncludeSourceRevisionInInformationalVersion is now off. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 18 +- .github/workflows/release.yml | 6 + CHANGELOG.md | 27 ++- README.md | 56 ++++++ VERSION.md | 2 +- scripts/smoke-test.ps1 | 160 ++++++++++++++++++ scripts/smoke-test.sh | 135 +++++++++++++++ .../TargetApiSimulator.csproj | 3 + 8 files changed, 391 insertions(+), 16 deletions(-) create mode 100644 scripts/smoke-test.ps1 create mode 100644 scripts/smoke-test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 699ccf5..2976169 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,20 +124,10 @@ jobs: sleep 1 done - BODY=$(curl -s -X POST http://localhost:5000/api/target \ - -H 'Content-Type: application/json' -d '{"a":1}') - test "$BODY" = "true" - - CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:5000/api/target \ - -H 'Content-Type: application/json' -d 'nope') - test "$CODE" = "400" - - # The 1 MB request body limit. TestServer does not enforce MaxRequestBodySize, so this - # is the only place the limit can be verified end to end. - python3 -c "open('big.json','w').write('{\"a\":\"' + 'x' * 1100000 + '\"}')" - CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:5000/api/target \ - -H 'Content-Type: application/json' --data-binary @big.json) - test "$CODE" = "413" + # The same script that ships inside every release zip. Running it here means the + # shipped verification tool cannot rot unnoticed, and it covers the request body + # limit, which TestServer does not enforce and unit tests therefore cannot reach. + bash scripts/smoke-test.sh http://localhost:5000 docker logs tas docker rm -f tas diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97a73f7..da40fb1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -178,6 +178,10 @@ jobs: [[ "$RID" == win-* ]] || chmod +x "out/$RID/TargetApiSimulator" + # Shipped alongside the binary so anyone can confirm a download actually works. + cp scripts/smoke-test.ps1 scripts/smoke-test.sh "out/$RID/" + chmod +x "out/$RID/smoke-test.sh" + mkdir -p dist ( cd "out/$RID" && zip -r "$GITHUB_WORKSPACE/dist/$NAME.zip" . ) ( cd dist && sha256sum "$NAME.zip" > "$NAME.zip.sha256" ) @@ -226,6 +230,8 @@ jobs: set -euo pipefail NAME="TargetApiSimulator-$VERSION-portable" rm -f out/portable/web.config out/portable/TargetApiSimulator.staticwebassets.endpoints.json + cp scripts/smoke-test.ps1 scripts/smoke-test.sh out/portable/ + chmod +x out/portable/smoke-test.sh mkdir -p dist ( cd out/portable && zip -r "$GITHUB_WORKSPACE/dist/$NAME.zip" . ) ( cd dist && sha256sum "$NAME.zip" > "$NAME.zip.sha256" ) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d14fd5..9e6dff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,30 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [1.0.4] - 2026-07-31 + +### Added + +- `smoke-test.ps1` and `smoke-test.sh` ship inside every release zip. Start the simulator, run + one of them, and it exercises the whole documented contract — 25 checks — printing PASS or + FAIL for each and exiting non-zero if anything is wrong, so it doubles as a CI gate. Exits + with 2, and says so, when the simulator is not reachable. +- README: a browser recipe. `/healthz` and `/version` open directly; for the POST endpoint, + open `/healthz` first and use `fetch` from the console — being on the simulator's own origin + is what avoids CORS, since no CORS policy is configured. + +### Changed + +- The container smoke test in CI now runs `scripts/smoke-test.sh` instead of a handful of inline + curl calls, so the script that ships to users is exercised on every pull request and cannot + rot unnoticed. + +### Fixed + +- `/version` reported the commit sha twice — `1.0.3+.` — because the release pipeline + appends it to `InformationalVersion` and the SDK appended it again. + `IncludeSourceRevisionInInformationalVersion` is now off. + ## [1.0.3] - 2026-07-31 ### Changed @@ -138,7 +162,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Every pull request runs `dotnet list package --vulnerable --include-transitive` and fails on a hit. CLI output is forced to English so the check cannot silently pass on a localised runner. -[Unreleased]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.3...HEAD +[Unreleased]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.4...HEAD +[1.0.4]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.3...v1.0.4 [1.0.3]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.2...v1.0.3 [1.0.2]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.0...v1.0.1 diff --git a/README.md b/README.md index f3452e1..12994ee 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,62 @@ and assert that what you send is well-formed JSON. A ready-to-run request collection is in [TargetApiSimulator.http](./TargetApiSimulator.http). +## Check that it works + +Every release zip contains **`smoke-test.ps1`** and **`smoke-test.sh`**. Start the simulator, run one of +them, and it exercises the whole documented contract — 25 checks — printing PASS or FAIL for each and +exiting non-zero if anything is wrong, so it also works as a CI gate. + +```bash +./TargetApiSimulator --urls http://localhost:5000 # in one terminal +./smoke-test.sh # in another +``` + +```powershell +.\TargetApiSimulator.exe --urls http://localhost:5000 +.\smoke-test.ps1 +``` + +``` +Service endpoints + PASS GET /healthz returns 200 + PASS GET /healthz body + ... +Contract details + PASS Response declares JSON content type + PASS Nesting deeper than 64 -> 400 + PASS Body over 1 MB -> 413 + +All 25 checks passed. +``` + +Pass a different address as the first argument (`./smoke-test.sh http://localhost:8080`, or +`-BaseUrl` in PowerShell). If the simulator is not running the script says so and exits with 2. + +### From the browser + +`http://localhost:5000/healthz` and `http://localhost:5000/version` open directly — they are plain GETs. + +The validation endpoint only accepts POST, so a URL alone will not reach it. **First open +`http://localhost:5000/healthz`**, then press F12 and paste this into the console. Being on the +simulator's own origin is what makes it work without CORS: + +```js +const check = async (body) => { + const r = await fetch('/api/target', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body + }); + console.log(r.status, await r.text()); +}; + +await check('{"key":"value"}'); // 200 true +await check('not json at all'); // 400 {"ErrorMessage": "This is not JSON"} +``` + +Whatever you send shows up in the simulator's console at the same moment. + ## See it running Send a payload, get a verdict: diff --git a/VERSION.md b/VERSION.md index d54973c..5556230 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,6 +1,6 @@ # Version -1.0.3 +1.0.4 true + + false