// 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); });