91 lines
3.4 KiB
TypeScript
91 lines
3.4 KiB
TypeScript
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<string | undefined> = [];
|
|
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/);
|
|
});
|
|
});
|