dev1/backend/tests/support_limits.mjs

175 lines
6.1 KiB
JavaScript

// Run:
// node backend/tests/support_limits.mjs
// BASE=https://staging.aptivaai.com ALLOW_NON_DEV=1 node backend/tests/support_limits.mjs
//
// Behavior:
// - Cookie-based auth only (new user each run, using jcoakley@aptivaai.com by default)
// - Unauth /api/support → 401
// - First + immediate duplicate → each may be 2xx/202 (ok), 429 (rate-limited), or 503 (no SENDGRID)
// Dedupe happens before SENDGRID check, so if first is 503, duplicate often 202 (unless rate-limited).
// We accept {200,201,202,204,429,503} for each, and require that at least one is not 429.
//
import assert from 'node:assert/strict';
const BASE = process.env.BASE || 'https://dev1.aptivaai.com';
if (BASE !== 'https://dev1.aptivaai.com' && process.env.ALLOW_NON_DEV !== '1') {
console.error(`Refusing to run against non-dev BASE='${BASE}'. Set ALLOW_NON_DEV=1 to override.`);
process.exit(2);
}
const j = (o) => JSON.stringify(o);
const rand = () => Math.random().toString(36).slice(2, 10);
const email = process.env.QA_EMAIL || 'jcoakley@aptivaai.com';
const username = `qa_${rand()}`;
const password = `Aa1!${rand()}Z`;
let cookie = ''; // session cookie (auth)
function captureSetCookie(headers) {
const sc = headers.get('set-cookie');
if (sc) cookie = sc.split(';')[0];
}
async function req(path, { method = 'GET', headers = {}, body } = {}) {
const h = {
'Content-Type': 'application/json',
...(cookie ? { Cookie: cookie } : {}),
...headers,
};
const res = await fetch(`${BASE}${path}`, {
method,
headers: h,
body: body ? j(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch {}
return { res, text, json };
}
async function reqNoAuth(path, { method = 'GET', headers = {}, body } = {}) {
const h = { 'Content-Type': 'application/json', ...headers };
const res = await fetch(`${BASE}${path}`, {
method, headers: h, body: body ? j(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch {}
return { res, text, json };
}
(async () => {
// Register
{
const { res, json } = await req('/api/register', {
method: 'POST',
body: {
username,
password,
firstname: 'QA',
lastname: 'Bot',
email,
zipcode: '30024',
state: 'GA',
area: 'Atlanta',
career_situation: 'planning',
},
});
assert.equal(res.status, 201, `register should 201, got ${res.status}`);
captureSetCookie(res.headers);
assert.ok(cookie, 'session cookie must be set after register');
}
// Sign in (refresh cookie)
{
const { res, json } = await req('/api/signin', {
method: 'POST',
body: { username, password },
});
assert.equal(res.status, 200, `signin should 200, got ${res.status}`);
assert.ok(json && typeof json.message === 'string', 'signin returns { message }');
captureSetCookie(res.headers);
}
// Unauthenticated request should 401
{
const { res } = await reqNoAuth('/api/support', {
method: 'POST',
body: { subject: 'unauth test', category: 'general', message: 'unauth test message' },
});
assert.equal(res.status, 401, `unauth /api/support should 401, got ${res.status}`);
}
// First + duplicate: allow {200,201,202,204,429,503}; require at least one NOT 429
const dedupePayload = {
subject: `QA support ${Date.now()}`,
category: 'technical',
message: `QA support test ${Date.now()}`
};
const first = await req('/api/support', { method: 'POST', body: dedupePayload });
const dup = await req('/api/support', { method: 'POST', body: dedupePayload });
const valid = (s) => [200,201,202,204,429,503].includes(s);
if (!valid(first.res.status)) {
throw new Error(`/api/support first unexpected ${first.res.status}, body=${first.text.slice(0,120)}`);
}
if (!valid(dup.res.status)) {
throw new Error(`/api/support duplicate unexpected ${dup.res.status}, body=${dup.text.slice(0,120)}`);
}
const anyNot429 = [first.res.status, dup.res.status].some((s) => s !== 429);
if (!anyNot429) {
throw new Error(`/api/support first+duplicate were both 429 (statuses: ${first.res.status}, ${dup.res.status})`);
}
console.log('✓ SUPPORT: unauth→401, first+dup→(allowed with ≥1 non-429) — starting burst…');
// Burst to trigger rate limit (unique messages to avoid dedupe masking)
const N = Number(process.env.BURST || 20);
const tasks = Array.from({ length: N }, (_, i) =>
req('/api/support', {
method: 'POST',
body: {
subject: `burst ${i}`,
category: 'technical',
message: `burst ${i} ${Date.now()} ${rand()}`
},
})
);
const results = await Promise.all(tasks);
const codes = results.map(r => r.res.status);
const allowed = new Set([200,201,202,204,429,503]);
const rlCount = codes.filter(c => c === 429).length;
if (!codes.every(c => allowed.has(c))) {
throw new Error(`unexpected status in burst: ${codes.join(',')}`);
}
if (rlCount < 1) {
throw new Error(`expected at least one 429 during burst; got codes=${codes.join(',')}`);
}
// Negative cases: invalid category and too-short message
{
const badCat = await req('/api/support', {
method: 'POST',
body: { subject: 'x', category: 'nope', message: 'valid message content' }
});
if (badCat.res.status !== 400 && badCat.res.status !== 429) {
// Allow 429 if limiter tripped; otherwise require 400 for invalid category
throw new Error(`/api/support invalid category expected 400 or 429, got ${badCat.res.status}`);
}
}
{
const tooShort = await req('/api/support', {
method: 'POST',
body: { subject: 'x', category: 'general', message: 'hi' } // < 5 chars
});
if (tooShort.res.status !== 400 && tooShort.res.status !== 429) {
throw new Error(`/api/support short message expected 400 or 429, got ${tooShort.res.status}`);
}
}
console.log('✓ SUPPORT: unauth→401, first+dup→(allowed with ≥1 non-429), burst→(allowed 2xx/429/503 with ≥1 429), negatives→400/429');
})().catch((e) => {
console.error('✖ SUPPORT regression failed:', e?.message || e);
process.exit(1);
});