// v1.1 smoke: dev IdP login, policy-enforced DM cap / group membership, listThreads, // and threaded replies — all over REST. Requires the service with IIOS_DEV_TOKENS=1. import 'dotenv/config'; const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200'; const assert = (c, m) => { if (!c) { console.error('✗', m); process.exit(1); } console.log('✓', m); }; async function login(username, password) { const r = await fetch(`${SERVICE}/v1/dev/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username, password }), }); return { status: r.status, body: r.ok ? await r.json() : null }; } function call(token, path, method = 'GET', body) { return fetch(`${SERVICE}${path}`, { method, headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: body ? JSON.stringify(body) : undefined, }); } async function json(token, path, method = 'GET', body) { const r = await call(token, path, method, body); if (!r.ok) throw new Error(`${method} ${path} ${r.status}: ${await r.text()}`); return r.json(); } // 1) dev IdP: correct password issues a token; wrong password is rejected. const ok = await login('alice', 'alice'); assert(ok.body?.token, 'dev IdP: alice logs in with password'); const bad = await login('alice', 'nope'); assert(bad.status === 401, 'dev IdP: wrong password → 401'); const alice = ok.body.token; // 2) DM is capped at two (policy). const dm = await json(alice, '/v1/threads', 'POST', { membership: 'dm' }); assert(dm.threadId, `alice created a DM (${dm.threadId})`); const add2 = await json(alice, `/v1/threads/${dm.threadId}/participants`, 'POST', { userId: 'bob' }); assert(add2.participantCount === 2, 'added bob → 2 participants'); const add3 = await call(alice, `/v1/threads/${dm.threadId}/participants`, 'POST', { userId: 'carol' }); assert(add3.status === 403, `adding a 3rd to a DM → 403 policy denied (${add3.status})`); // 3) group: creator (ADMIN) can add several. const grp = await json(alice, '/v1/threads', 'POST', { membership: 'group', creatorRole: 'ADMIN' }); await json(alice, `/v1/threads/${grp.threadId}/participants`, 'POST', { userId: 'bob' }); const g3 = await json(alice, `/v1/threads/${grp.threadId}/participants`, 'POST', { userId: 'carol' }); assert(g3.participantCount === 3, 'group admin added bob + carol → 3 participants'); // 4) threaded reply round-trips. const first = await json(alice, `/v1/threads/${grp.threadId}/messages`, 'POST', { content: 'question?' }); const reply = await json(alice, `/v1/threads/${grp.threadId}/messages`, 'POST', { content: 'answer', parentInteractionId: first.id }); assert(reply.parentInteractionId === first.id, 'a reply carries parentInteractionId'); // 5) listThreads shows the caller's threads with membership + last message. const mine = await json(alice, '/v1/threads'); const g = mine.find((t) => t.threadId === grp.threadId); assert(g && g.membership === 'group' && g.participantCount === 3, 'GET /v1/threads lists the group with membership + count'); assert(g.lastMessage === 'answer', 'thread summary carries the last message'); console.log('\nv1.1 membership + replies smoke: PASS'); process.exit(0);