175 lines
6.1 KiB
JavaScript
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);
|
|
});
|