Files
terminalShop-old/loadtest/overnight.js
marcusDenslow 327bb9c867
Some checks failed
Deploy / deploy (push) Has been cancelled
cleanup
2026-05-02 11:28:28 +02:00

226 lines
6.7 KiB
JavaScript

import http from 'k6/http';
const BASE = __ENV.BASE_URL || 'https://api.sshops.uk';
const SECRET = __ENV.CLIENT_SECRET || '';
// Per-VU JWT cache. JWTs are valid for 30 minutes; we refresh ~5 min early.
let cachedToken = null;
let cachedExpiresAt = 0;
let cachedFingerprint = null;
// SSHPublicKey has a uniqueIndex constraint, so each VU needs a distinct
// (fake but unique) public key string. Suffixing with VU+timestamp keeps
// the full text unique without affecting the rest of the body.
let cachedPubkey = null;
function getToken() {
const now = Date.now();
if (cachedToken && now < cachedExpiresAt) return cachedToken;
if (!cachedFingerprint) {
cachedFingerprint = `SHA256:k6-${__VU}-${now.toString(36)}`;
cachedPubkey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoadTest${__VU}-${now.toString(36)} k6@vu${__VU}`;
}
const res = http.post(
`${BASE}/api/v1/auth/token`,
JSON.stringify({
client_secret: SECRET,
fingerprint: cachedFingerprint,
ssh_public_key: cachedPubkey,
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth_token_refresh' } },
);
if (res.status !== 200) return null;
const body = JSON.parse(res.body);
cachedToken = body.access_token;
cachedExpiresAt = now + (body.expires_in - 300) * 1000;
return cachedToken;
}
function authHeaders() {
const tok = getToken();
return tok ? { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' } : null;
}
export const options = {
discardResponseBodies: true,
scenarios: {
browse: {
executor: 'ramping-arrival-rate',
startRate: 6,
timeUnit: '1m',
preAllocatedVUs: 10,
maxVUs: 50,
stages: [
{ target: 12, duration: '1h' },
{ target: 30, duration: '2h' },
{ target: 90, duration: '2h' },
{ target: 60, duration: '2h' },
{ target: 120, duration: '3h' },
{ target: 30, duration: '2h' },
],
exec: 'browse',
},
authed_browse: {
executor: 'ramping-arrival-rate',
startRate: 2,
timeUnit: '1m',
preAllocatedVUs: 5,
maxVUs: 20,
stages: [
{ target: 10, duration: '4h' },
{ target: 30, duration: '4h' },
{ target: 8, duration: '4h' },
],
exec: 'authedBrowse',
},
convert_fail: {
// /cart/convert is rate-limited 10/min per user. We share one loadtest
// user across all VUs so ceiling is 10/min; we stay under at 4/min.
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1m',
preAllocatedVUs: 3,
maxVUs: 5,
stages: [
{ target: 2, duration: '4h' },
{ target: 4, duration: '4h' },
{ target: 2, duration: '4h' },
],
exec: 'convertFail',
},
address_bad: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1m',
preAllocatedVUs: 3,
maxVUs: 5,
stages: [
{ target: 3, duration: '12h' },
],
exec: 'addressBad',
},
auth_warn: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1m',
preAllocatedVUs: 5,
maxVUs: 10,
stages: [
{ target: 3, duration: '4h' },
{ target: 10, duration: '4h' },
{ target: 5, duration: '4h' },
],
exec: 'authBad',
},
auth_lookup: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1m',
preAllocatedVUs: 5,
maxVUs: 10,
stages: [
{ target: 4, duration: '6h' },
{ target: 8, duration: '6h' },
],
exec: 'authLookup',
},
err_404: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1m',
preAllocatedVUs: 3,
maxVUs: 5,
stages: [
{ target: 5, duration: '12h' },
],
exec: 'notFound',
},
},
};
export default function () {
// Smoke-test entry point for `--vus N --duration T` runs.
browse();
authBad();
authLookup();
notFound();
if (SECRET) {
authedBrowse();
convertFail();
addressBad();
}
}
export function browse() {
const r = Math.random();
if (r < 0.6) http.get(`${BASE}/api/v1/products`, { tags: { name: 'list_products' } });
else if (r < 0.8) http.get(`${BASE}/api/v1/products/1`, { tags: { name: 'get_product' } });
else if (r < 0.95) http.get(`${BASE}/api/v1/health`, { tags: { name: 'health' } });
else http.get(`${BASE}/api/v1/ping`, { tags: { name: 'ping' } });
}
export function authedBrowse() {
const h = authHeaders();
if (!h) return;
const r = Math.random();
if (r < 0.4) http.get(`${BASE}/api/v1/cart`, { headers: h, tags: { name: 'authed_get_cart' } });
else if (r < 0.6) http.get(`${BASE}/api/v1/cards`, { headers: h, tags: { name: 'authed_list_cards' } });
else if (r < 0.8) http.get(`${BASE}/api/v1/addresses`, { headers: h, tags: { name: 'authed_list_addresses' } });
else http.get(`${BASE}/api/v1/orders`, { headers: h, tags: { name: 'authed_list_orders' } });
}
export function convertFail() {
const h = authHeaders();
if (!h) return;
// Reset cart, drop one item in, attempt convert. No card/address selected
// → ConvertCart hits validation_missing_address → cart_conversion_total
// outcome=validation_missing_address + audit.OrderFailed warn.
http.del(`${BASE}/api/v1/cart`, null, { headers: h, tags: { name: 'cart_clear' } });
http.put(
`${BASE}/api/v1/cart/item`,
JSON.stringify({ coffee_id: 1, quantity: 1 }),
{ headers: h, tags: { name: 'cart_add_item' } },
);
http.post(`${BASE}/api/v1/cart/convert`, null, { headers: h, tags: { name: 'cart_convert_fail' } });
}
export function addressBad() {
const h = authHeaders();
if (!h) return;
// Junk US zip + street → shippo validation fails → addressLog.Warn.
http.post(
`${BASE}/api/v1/addresses`,
JSON.stringify({
name: 'Load Test',
street: 'asdfasdfasdf',
city: 'Nowhere',
state: 'XX',
zip: '00000',
country: 'US',
phone: '+15555550000',
}),
{ headers: h, tags: { name: 'address_create_bad' } },
);
}
export function authBad() {
http.post(
`${BASE}/api/v1/auth/token`,
JSON.stringify({ client_secret: 'definitely_wrong', fingerprint: 'SHA256:fake' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth_bad_creds' } },
);
}
export function authLookup() {
const fp = `SHA256:${Math.random().toString(36).slice(2, 14)}`;
http.get(`${BASE}/api/v1/auth/user?fingerprint=${fp}`, { tags: { name: 'auth_user_lookup' } });
}
export function notFound() {
const id = 99000 + Math.floor(Math.random() * 1000);
http.get(`${BASE}/api/v1/products/${id}`, { tags: { name: 'product_not_found' } });
}