mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-26 10:18:46 +01:00
226 lines
6.7 KiB
JavaScript
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' } });
|
|
}
|