From 8853bf5f59ff444ce7faf03c84afd44d3334c52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Dvo=C5=99=C3=A1k?= <150935816+Dvorinka@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:03:00 +0200 Subject: [PATCH] test(load): k6 smoke for public booking surface (#22) First load script: healthz + meta/config + tenant availability/catalog, booking POST gated behind WRITE=1. Env-driven BASE_URL/VUS/DURATION so the same file runs locally (smoke) and from the Proxmox runner against a deployed target (load). 404 on unknown tenant slug counted as an expected response. Authored By: TDvorak Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/load/bookings.js | 73 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/load/bookings.js diff --git a/tests/load/bookings.js b/tests/load/bookings.js new file mode 100644 index 0000000..63cba1f --- /dev/null +++ b/tests/load/bookings.js @@ -0,0 +1,73 @@ +// k6 smoke/load test — Bookra public booking surface. +// +// Local smoke (script validation, not perf evidence — loopback latency is ~0ms): +// k6 run tests/load/bookings.js +// Load against a deployed target (run from the Proxmox host, not your box): +// BASE_URL=https://api.bookra.eu VUS=50 DURATION=3m k6 run tests/load/bookings.js +// Include a booking-creation pass (writes a real row — point at a test tenant): +// WRITE=1 TENANT_SLUG=test-tenant BASE_URL=... k6 run tests/load/bookings.js +// +// Endpoints exercised: /healthz, /v1/meta/config, public availability + catalog, +// and (WRITE=1) POST /v1/public/bookings — the money path, rate-limited. +import http from "k6/http"; +import { check, sleep, group } from "k6"; + +const BASE_URL = (__ENV.BASE_URL || "http://localhost:8080").replace(/\/$/, ""); +const TENANT = __ENV.TENANT_SLUG || "demo"; +const WRITE = __ENV.WRITE === "1"; + +export const options = { + scenarios: { + booking_surface: { + executor: "constant-vus", + vus: Number(__ENV.VUS || 2), + duration: __ENV.DURATION || "30s", + }, + }, + thresholds: { + // 800ms covers a cold local dev server; tighten to 400 once aimed at prod. + http_req_duration: ["p(95)<800"], + http_req_failed: ["rate<0.01"], + "checks{check:public reads ok}": ["rate>0.99"], + }, +}; + +export default function () { + group("public reads", () => { + // 404 on an unknown tenant slug is a correct answer, not a failure — + // mark it expected so http_req_failed stays meaningful. + const tenantOk = { responseCallback: http.expectedStatuses(200, 404) }; + const hz = http.get(`${BASE_URL}/healthz`); + const cfg = http.get(`${BASE_URL}/v1/meta/config`); + const cat = http.get(`${BASE_URL}/v1/public/tenants/${TENANT}/catalog`, tenantOk); + const av = http.get(`${BASE_URL}/v1/public/tenants/${TENANT}/availability`, tenantOk); + + check(null, { + "public reads ok": + hz.status === 200 && + cfg.status === 200 && + [200, 404].includes(cat.status) && + [200, 404].includes(av.status), + }); + }); + + if (WRITE) { + group("booking create", () => { + const res = http.post( + `${BASE_URL}/v1/public/bookings`, + JSON.stringify({ + tenantSlug: TENANT, + serviceId: __ENV.SERVICE_ID || "svc_loadtest", + startsAt: new Date(Date.now() + 864e5).toISOString(), + customer: { name: "k6 loadtest", email: "k6@example.invalid" }, + }), + { headers: { "Content-Type": "application/json" } }, + ); + // 201/200 created, 400/404 validation — all prove the handler runs; + // 5xx and 429-at-low-VUS are the failures that matter. + check(res, { "booking handler healthy": (r) => r.status < 500 }); + }); + } + + sleep(0.5); +}