Files
Cibello-app/scripts/loadtest.mjs
T
2026-08-05 19:21:11 +07:00

111 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Lasttest mot lokal API (körs mot dist-bygget):
* node apps/api/dist/index.js & # starta API:t först
* node scripts/loadtest.mjs
*
* Tre scenarier: oautentiserad healthz, autentiserad receptlista (DB + översättnings-
* upplösning) och rekommendationer (tyngsta läsvägen: lager + motor + förklaringar).
* Trösklar (encore-server, 1 CPU-klass): p99 < 500 ms, inga fel, > 100 req/s på recept.
*/
import autocannon from "autocannon";
const BASE = process.env.API_BASE_URL ?? "http://localhost:4000";
async function json(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
...options,
headers: { "content-type": "application/json", ...(options.headers ?? {}) },
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!res.ok) throw new Error(`${path} -> ${res.status}`);
return res.json();
}
// --- Setup: användare med hushåll + lite lager, så läsvägarna är realistiska ---
const email = `loadtest-${process.pid}@example.com`;
let auth;
try {
auth = await json("/v1/auth/register", {
method: "POST",
body: { email, password: "Lasttest1234!", displayName: "Lasttest" },
});
} catch {
auth = await json("/v1/auth/login", {
method: "POST",
body: { email, password: "Lasttest1234!" },
});
}
const token = auth.accessToken;
const headers = { authorization: `Bearer ${token}` };
const hh = await json("/v1/households", { method: "POST", headers, body: { name: "Lasttest" } });
const full = await json(`/v1/households/${hh.id}`, { headers });
const locId = full.storageLocations[0].id;
for (const [ing, qty] of [["pasta_spaghetti", 500], ["minced_beef", 400], ["onion_yellow", 3]]) {
await json("/v1/inventory/items", {
method: "POST",
headers,
body: {
ingredientId: ing,
displayName: ing,
storageLocationId: locId,
quantity: qty,
unit: qty > 10 ? "GRAM" : "COUNT",
},
}).catch(() => {});
}
// --- Scenarier ---
const scenarios = [
{ name: "healthz (baslinje)", path: "/healthz", auth: false },
{ name: "GET /v1/recipes (lista + i18n-upplösning)", path: "/v1/recipes?limit=20", auth: true },
{
name: "GET /v1/recommendations/what-to-eat (tyngsta läsvägen)",
path: "/v1/recommendations/what-to-eat?meal=dinner",
auth: true,
},
];
const results = [];
for (const s of scenarios) {
const r = await autocannon({
url: `${BASE}${s.path}`,
connections: 25,
duration: 10,
headers: s.auth ? headers : {},
});
results.push({
scenario: s.name,
reqPerSec: Math.round(r.requests.average),
latencyP50: r.latency.p50,
latencyP99: r.latency.p99,
errors: r.errors + r.non2xx,
});
console.log(
`${s.name}: ${Math.round(r.requests.average)} req/s, p50 ${r.latency.p50} ms, p99 ${r.latency.p99} ms, fel ${r.errors + r.non2xx}`,
);
}
// --- Trösklar ---
const failures = [];
for (const r of results) {
if (r.errors > 0) failures.push(`${r.scenario}: ${r.errors} fel`);
if (r.latencyP99 > 500) failures.push(`${r.scenario}: p99 ${r.latencyP99} ms > 500 ms`);
}
const recipes = results[1];
if (recipes && recipes.reqPerSec < 100) {
failures.push(`receptlistan: ${recipes.reqPerSec} req/s < 100`);
}
console.log("\n| Scenario | req/s | p50 | p99 | fel |");
console.log("| -------- | ----- | --- | --- | --- |");
for (const r of results) {
console.log(
`| ${r.scenario} | ${r.reqPerSec} | ${r.latencyP50} ms | ${r.latencyP99} ms | ${r.errors} |`,
);
}
if (failures.length) {
console.error("\nUNDERKÄND:\n" + failures.map((f) => `${f}`).join("\n"));
process.exit(1);
}
console.log("\nGODKÄND alla trösklar klarade.");