This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user