Files
pig/apps/api/test/google-sheets.test.ts
2026-08-13 01:39:01 -07:00

85 lines
3.7 KiB
TypeScript

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { GOOGLE_OAUTH_SCOPES } from '../src/services/google-sheets';
import {
buildGoogleAuthorizationUrl,
googleConnectionMetadata,
normaliseGoogleValues,
oauthFlowMatches,
oauthStateHash,
parseBoundedGoogleRange,
} from '../src/services/google-sheets';
describe('Google OAuth proof and redaction', () => {
it('binds state and PKCE without putting the verifier in the authorization URL', () => {
const state = 'state-secret';
const verifier = 'verifier-secret';
const url = new URL(buildGoogleAuthorizationUrl({
clientId: 'client-id',
redirectUri: 'https://pig.example/oauth/google/callback',
state,
challenge: 'challenge',
}));
assert.equal(url.searchParams.get('state'), state);
assert.equal(url.searchParams.get('code_challenge'), 'challenge');
assert.equal(url.searchParams.get('code_challenge_method'), 'S256');
assert.equal(url.searchParams.get('scope'), GOOGLE_OAUTH_SCOPES.join(' '));
assert.equal(url.searchParams.get('access_type'), 'offline');
assert.equal(url.toString().includes(verifier), false);
});
it('accepts only the matching, unexpired, just-consumed state', () => {
const now = new Date('2026-08-13T12:00:00.000Z');
const flow = {
stateHash: oauthStateHash('expected'),
browserBindingHash: oauthStateHash('browser'),
expiresAt: new Date('2026-08-13T12:01:00.000Z'),
consumedAt: now,
};
assert.equal(oauthFlowMatches(flow, 'expected', 'browser', now), true);
assert.equal(oauthFlowMatches(flow, 'attacker', 'browser', now), false);
assert.equal(oauthFlowMatches(flow, 'expected', 'other-browser', now), false);
assert.equal(oauthFlowMatches({ ...flow, consumedAt: null }, 'expected', 'browser', now), false);
assert.equal(oauthFlowMatches({ ...flow, expiresAt: now }, 'expected', 'browser', now), false);
});
it('never serializes encrypted tokens in connection metadata', () => {
const metadata = googleConnectionMetadata(true, {
userId: '00000000-0000-4000-8000-000000000001',
refreshTokenEncrypted: 'v1.refresh.secret',
accessTokenEncrypted: 'v1.access.secret',
accessTokenExpiresAt: new Date(),
scopes: [...GOOGLE_OAUTH_SCOPES],
connectedAt: new Date('2026-08-13T12:00:00.000Z'),
updatedAt: new Date(),
});
const serialized = JSON.stringify(metadata);
assert.equal(serialized.includes('v1.refresh.secret'), false);
assert.equal(serialized.includes('v1.access.secret'), false);
assert.deepEqual(Object.keys(metadata), ['configured', 'connected', 'connectedAt', 'scopes']);
});
});
describe('Google Sheets range and value boundaries', () => {
it('requires an explicit rectangular range within A14 limits and the selected grid', () => {
assert.deepEqual(parseBoundedGoogleRange('a1:CV2001', { rowCount: 3_000, columnCount: 100 }), {
a1: 'A1:CV2001',
rows: 2_001,
columns: 100,
});
assert.throws(() => parseBoundedGoogleRange('A:Z'));
assert.throws(() => parseBoundedGoogleRange('A1:C2002'));
assert.throws(() => parseBoundedGoogleRange('A1:C10', { rowCount: 9, columnCount: 3 }));
});
it('normalizes formatted values into A14 rows while keeping text inert', () => {
const table = normaliseGoogleValues([
['external_id', 'name', 'active', 'score'],
[7, '=IMPORTDATA("https://example.test")', true, 2.5],
], 4);
assert.deepEqual(table.headers, ['external_id', 'name', 'active', 'score']);
assert.deepEqual(table.rows, [['7', '=IMPORTDATA("https://example.test")', 'true', '2.5']]);
assert.match(table.warnings.join(' '), /formula source was not imported/i);
});
});