import test from 'node:test'; import assert from 'node:assert/strict'; import { createHmac } from 'node:crypto'; import worker from '../src/index.js'; const POST = '01900000-0000-7000-8000-000000000001'; const COMMENT = '01900000-0000-7000-8000-000000000002'; const OWNER = '9951229e-eea5-4376-96f4-78989bc3a506'; const OTHER = 'ee2f39ac-fe52-4cfb-b4e3-4ae1d114c916'; const env = { SESSION_COOKIE_NAME: 'fixture', SESSION_SECRET: 'test-session-key', ABC_API_KEY: 'fixture-api-key', ABC_PROJECT_ID: 'project', ABC_CHANNEL_ID: 'channel' }; const feedback = (extra = {}) => ({ id: POST, requester_id: OWNER, is_secret: true, title: 'PRIVATE_TITLE', contents: 'PRIVATE_CONTENT', requester_email: 'PRIVATE_EMAIL', attachments: [{ url: 'https://example.test/PRIVATE_ATTACHMENT' }], comments: [{ content: 'PRIVATE_COMMENT' }], ...extra }); const response = (body, status = 200) => Response.json(body, { status }); function request(path = '', method = 'GET', viewer = OTHER, body) { const user = { requesterId: viewer, userUuid: OWNER, ssoSubject: OWNER, requesterTenantId: 'shared-tenant', name: 'same-name', email: 'PRIVATE_EMAIL', roles: ['ADMIN', 'SUPPORT'] }; const payload = Buffer.from(JSON.stringify({ user, expiresAt: Date.now() + 60000 })).toString('base64url'); const signature = createHmac('sha256', env.SESSION_SECRET).update(payload).digest('base64url'); return new Request('https://qa.example.test/api/feedbacks' + path, { method, headers: { cookie: `${env.SESSION_COOKIE_NAME}=${payload}.${signature}`, 'content-type': 'application/json', 'x-requester-id': OWNER }, body: body === undefined ? undefined : JSON.stringify(body) }); } async function upstream(handler, run) { const original = globalThis.fetch; const calls = []; globalThis.fetch = async (url, init) => { calls.push({ url: String(url), ...init }); return handler(url, init); }; try { await run(calls); } finally { globalThis.fetch = original; } } test('list masks private data before sending it to a different signed-in user and keeps pagination', async () => { await upstream(() => response({ items: [feedback(), feedback({ id: COMMENT, is_secret: false })], meta: { total: 102, totalPages: 2, page: 1 } }), async calls => { const result = await worker.fetch(request(), env); const { items, meta } = await result.json(); assert.equal(result.headers.get('cache-control'), 'no-store'); assert.equal(items[0].title, '비밀글입니다.'); assert.equal(items[0].access_restricted, true); assert.doesNotMatch(JSON.stringify(items[0]), /PRIVATE_/); assert.equal(items[1].contents, 'PRIVATE_CONTENT'); assert.deepEqual(meta, { total: 102, totalPages: 2, page: 1 }); assert.equal(calls[0].headers['x-requester-id'], OTHER); }); }); for (const flag of [true, 'true', 1, '1', 'TRUE', 'yes']) { for (const wrap of [value => value, value => ({ data: value }), value => ({ feedback: value })]) { test(`private detail refuses non-owner even when upstream returns 200 (${String(flag)}, ${wrap.toString()})`, async () => { await upstream(() => response(wrap(feedback({ is_secret: flag }))), async () => { const result = await worker.fetch(request('/' + POST), env); assert.equal(result.status, 403); assert.doesNotMatch(await result.text(), /PRIVATE_/); }); }); } } for (const fields of [ { fields: { is_secret: 'true', requester_id: OWNER } }, { extra_fields: { is_secret: true, requester_id: OWNER } }, { metadata: { isSecret: true, requesterId: OWNER } }, { fields: [{ key: 'is_secret', value: 'true' }, { key: 'requester_id', value: OWNER }] }, { fields: [{ key: 'is_secret', value: 'false' }, { key: 'is_secret', value: 'true' }] }, { is_secret: false, metadata: { is_secret: true } }, { is_secret: 'unexpected-value' } ]) { test(`nested and conflicting privacy metadata is protected: ${JSON.stringify(fields)}`, async () => { const record = { id: POST, title: 'PRIVATE_TITLE', contents: 'PRIVATE_CONTENT', ...fields }; await upstream(() => response(record), async () => { const result = await worker.fetch(request('/' + POST), env); assert.equal(result.status, 403); assert.doesNotMatch(await result.text(), /PRIVATE_/); }); }); } test('private records with missing author ID are denied; the owner still receives their own record', async () => { for (const ownerId of ['', OWNER.toUpperCase()]) { await upstream(() => response(feedback({ requester_id: ownerId })), async () => { const result = await worker.fetch(request('/' + POST, 'GET', OWNER), env); assert.equal(result.status, ownerId ? 200 : 403); if (ownerId) assert.equal((await result.json()).feedback.contents, 'PRIVATE_CONTENT'); }); } }); for (const [path, method, body] of [ ['', 'PATCH', { title: 'attempt' }], ['', 'DELETE'], ['/comments', 'GET'], ['/comments', 'POST', { content: 'attempt' }], ['/comments/' + COMMENT, 'PATCH', { content: 'attempt' }], ['/comments/' + COMMENT, 'DELETE'], ['/completion-confirmation', 'POST'] ]) { test(`private parent blocks direct ${method} ${path || 'detail'} before comments or mutations reach upstream`, async () => { await upstream(() => response(feedback()), async calls => { const result = await worker.fetch(request('/' + POST + path, method, OTHER, body), env); assert.equal(result.status, 403); assert.doesNotMatch(await result.text(), /PRIVATE_/); assert.equal(calls.length, 1); assert.ok(calls[0].url.endsWith('/feedbacks/' + POST)); assert.equal(calls[0].method, 'GET'); assert.equal(calls[0].headers['x-requester-id'], OTHER); }); }); } test('comment access fails closed when the parent cannot be loaded', async () => { for (const status of [403, 404, 503]) { await upstream(() => response({ message: 'PRIVATE_ERROR' }, status), async calls => { const result = await worker.fetch(request('/' + POST + '/comments'), env); assert.equal(result.status, status === 503 ? 502 : status); assert.doesNotMatch(await result.text(), /PRIVATE_/); assert.equal(calls.length, 1); }); } }); test('admin-key search cannot reveal private words via matching locked records', async () => { await upstream(() => assert.fail('private content must not be queried through the admin search API'), async () => { const result = await worker.fetch(request('?searchText=PRIVATE_CONTENT&fieldKey=contents'), env); assert.equal(result.status, 400); }); }); for (const isSecret of [false, true]) { test(`${isSecret ? 'own private' : 'public'} feedback still permits reading comments`, async () => { await upstream((url) => response(String(url).includes('/comments') ? { comments: [{ content: 'allowed' }] } : feedback({ is_secret: isSecret })), async calls => { const result = await worker.fetch(request('/' + POST + '/comments', 'GET', isSecret ? OWNER : OTHER), env); assert.equal(result.status, 200); assert.equal((await result.json()).comments[0].content, 'allowed'); assert.equal(calls.length, 2); }); }); }