import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; import { collectNotionPages, createNotionOAuthAttempt, flattenNotionProperty, notionAuthorizationUrl, notionConnectionMetadata, verifyNotionOAuthAttempt, } from '../src/services/notion'; describe('Notion OAuth decisions', () => { it('uses one-time state and browser binding without inventing unsupported PKCE parameters', () => { let byte = 0; const attempt = createNotionOAuthAttempt( new Date('2026-08-13T12:00:00.000Z'), (size) => Buffer.alloc(size, byte += 1), ); assert.notEqual(attempt.state, attempt.verifier); assert.notEqual(attempt.stateHash, attempt.state); assert.notEqual(attempt.verifierHash, attempt.verifier); assert.equal(verifyNotionOAuthAttempt( attempt.verifier, attempt.verifierHash, attempt.expiresAt, new Date('2026-08-13T12:09:59.000Z'), ), true); assert.equal(verifyNotionOAuthAttempt('tampered', attempt.verifierHash, attempt.expiresAt), false); assert.equal(verifyNotionOAuthAttempt( attempt.verifier, attempt.verifierHash, attempt.expiresAt, new Date('2026-08-13T12:10:00.000Z'), ), false); const url = new URL(notionAuthorizationUrl({ clientId: 'client-id', redirectUri: 'https://pig.example/api/imports/notion/oauth/callback', state: attempt.state, })); assert.equal(url.searchParams.get('state'), attempt.state); assert.equal(url.searchParams.has('code_challenge'), false); assert.equal(url.searchParams.has('code_verifier'), false); }); it('redacts every credential-shaped field from connection metadata', () => { const storedConnection = { id: 'connection-id', workspaceId: 'workspace-id', workspaceName: 'Sales', workspaceIcon: null, createdAt: new Date('2026-08-13T12:00:00.000Z'), credentialsEncrypted: 'v1.secret.envelope', accessToken: 'never-return', }; const metadata = notionConnectionMetadata(storedConnection); const serialized = JSON.stringify(metadata); assert.equal(serialized.includes('never-return'), false); assert.equal(serialized.includes('envelope'), false); }); }); describe('Notion pagination and flattening decisions', () => { it('follows cursors in order and stops after the declared import bound', async () => { const cursors: Array = []; const rows = await collectNotionPages(async (cursor) => { cursors.push(cursor); return cursor ? { results: [{ id: '2' }, { id: '3' }], has_more: false, next_cursor: null } : { results: [{ id: '1' }], has_more: true, next_cursor: 'next' }; }, 2); assert.deepEqual(cursors, [undefined, 'next']); assert.deepEqual(rows.map((row) => row.id), ['1', '2']); }); it('maps supported values explicitly and rejects unstable property types', () => { assert.deepEqual(flattenNotionProperty({ type: 'title', title: [{ plain_text: 'Acme' }, { plain_text: ' Compute' }], }), { value: 'Acme Compute' }); assert.deepEqual(flattenNotionProperty({ type: 'date', date: { start: '2026-09-01', end: '2026-09-30' }, }), { value: '2026-09-01/2026-09-30' }); assert.deepEqual(flattenNotionProperty({ type: 'formula', formula: { type: 'number', number: 12.5 }, }), { value: '12.5' }); assert.match(flattenNotionProperty({ type: 'button', button: {} }).error ?? '', /stable tabular/); }); });