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); +}