test(telemetry): add route-logic tests — containsPII, computePk, normalizeMessage, fingerprint, policyMatch, mergePolicies (34→77 tests)

This commit is contained in:
saravanakumardb1 2026-02-17 09:42:57 -08:00
parent dcc0befb8c
commit 20f77d5a50
2 changed files with 342 additions and 6 deletions

View File

@ -48,18 +48,18 @@ const PII_PATTERNS = [
/\b\d{3}-\d{2}-\d{4}\b/, // SSN
];
function containsPII(text: string): boolean {
export function containsPII(text: string): boolean {
if (!PII_SCAN_ENABLED) return false;
return PII_PATTERNS.some(p => p.test(text));
}
function computePk(productId: string, occurredAt: string, platform: string): string {
export function computePk(productId: string, occurredAt: string, platform: string): string {
const date = new Date(occurredAt);
const yyyyMM = `${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
return `${productId}:${yyyyMM}:${platform}`;
}
function normalizeMessage(msg: string): string {
export function normalizeMessage(msg: string): string {
return msg
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
.replace(/\d+/g, '<N>')
@ -68,7 +68,7 @@ function normalizeMessage(msg: string): string {
.trim();
}
function generateFingerprint(event: TelemetryEventDoc): string {
export function generateFingerprint(event: TelemetryEventDoc): string {
const input = [
event.platform,
event.channel,
@ -102,7 +102,10 @@ interface ClientContext {
regionCode?: string;
}
function policyMatchesContext(policy: TelemetryCollectionPolicyDoc, ctx: ClientContext): boolean {
export function policyMatchesContext(
policy: TelemetryCollectionPolicyDoc,
ctx: ClientContext
): boolean {
const t = policy.targeting;
// Check time bounds
@ -164,7 +167,7 @@ function policyMatchesContext(policy: TelemetryCollectionPolicyDoc, ctx: ClientC
return true;
}
function mergePolicies(policies: TelemetryCollectionPolicyDoc[]): TelemetryCollectionConfig {
export function mergePolicies(policies: TelemetryCollectionPolicyDoc[]): TelemetryCollectionConfig {
if (policies.length === 0) {
// Hardcoded default
return {

View File

@ -9,7 +9,17 @@ import {
CreatePolicySchema,
UpdatePolicySchema,
TelemetryQuerySchema,
type TelemetryEventDoc,
type TelemetryCollectionPolicyDoc,
} from './types.js';
import {
containsPII,
computePk,
normalizeMessage,
generateFingerprint,
policyMatchesContext,
mergePolicies,
} from './routes.js';
// ─── Minimal valid event for reuse ──────────────────────────────────
@ -315,3 +325,326 @@ describe('TelemetryQuerySchema', () => {
}
});
});
// ─── containsPII ────────────────────────────────────────────────────
describe('containsPII', () => {
it('detects email addresses', () => {
expect(containsPII('Contact user@example.com for help')).toBe(true);
});
it('detects US phone numbers', () => {
expect(containsPII('Call 555-123-4567')).toBe(true);
expect(containsPII('Call 5551234567')).toBe(true);
});
it('detects credit card numbers', () => {
expect(containsPII('Card 4111 1111 1111 1111')).toBe(true);
expect(containsPII('Card 4111-1111-1111-1111')).toBe(true);
});
it('detects SSN', () => {
expect(containsPII('SSN 123-45-6789')).toBe(true);
});
it('allows clean text', () => {
expect(containsPII('Recognition failed with code 209')).toBe(false);
});
it('allows empty string', () => {
expect(containsPII('')).toBe(false);
});
});
// ─── computePk ──────────────────────────────────────────────────────
describe('computePk', () => {
it('computes correct pk for mid-year date', () => {
expect(computePk('lysnrai', '2026-06-15T10:00:00.000Z', 'ios')).toBe('lysnrai:202606:ios');
});
it('pads single-digit months', () => {
expect(computePk('lysnrai', '2026-01-01T00:00:00.000Z', 'desktop')).toBe(
'lysnrai:202601:desktop'
);
});
it('handles December correctly', () => {
expect(computePk('mindlyst', '2026-12-31T23:59:59.000Z', 'web')).toBe('mindlyst:202612:web');
});
it('uses UTC month (not local)', () => {
// Feb 1 00:00 UTC — should be 202602 regardless of local timezone
expect(computePk('lysnrai', '2026-02-01T00:00:00.000Z', 'android')).toBe(
'lysnrai:202602:android'
);
});
});
// ─── normalizeMessage ───────────────────────────────────────────────
describe('normalizeMessage', () => {
it('replaces UUIDs', () => {
expect(normalizeMessage('Error for 550e8400-e29b-41d4-a716-446655440000')).toContain('<uuid>');
});
it('replaces numbers', () => {
expect(normalizeMessage('Timeout after 5000ms')).toBe('timeout after <n>ms');
});
it('replaces file paths', () => {
expect(normalizeMessage('Failed at /usr/lib/speech.so')).toContain('<path>');
});
it('lowercases', () => {
expect(normalizeMessage('FATAL ERROR')).toBe('fatal error');
});
it('trims whitespace', () => {
expect(normalizeMessage(' error ')).toBe('error');
});
it('handles empty string', () => {
expect(normalizeMessage('')).toBe('');
});
it('is deterministic', () => {
const msg = 'Error 550e8400-e29b-41d4-a716-446655440000 at /foo/bar.ts line 42';
expect(normalizeMessage(msg)).toBe(normalizeMessage(msg));
});
});
// ─── generateFingerprint ────────────────────────────────────────────
describe('generateFingerprint', () => {
function makeDoc(overrides: Partial<TelemetryEventDoc> = {}): TelemetryEventDoc {
return {
id: '550e8400-e29b-41d4-a716-446655440000',
productId: 'lysnrai',
anonymousInstallId: '660e8400-e29b-41d4-a716-446655440001',
sessionId: 'sess_abc',
platform: 'ios',
channel: 'keyboard_extension',
osFamily: 'ios',
appVersion: '1.0.0',
buildNumber: '26',
releaseChannel: 'beta',
eventType: 'error',
module: 'dictation',
eventName: 'recognition_failed',
occurredAt: '2026-02-17T08:00:00.000Z',
pk: 'lysnrai:202602:ios',
receivedAt: '2026-02-17T08:00:01.000Z',
ttl: 2592000,
...overrides,
};
}
it('returns 16-char hex string', () => {
const fp = generateFingerprint(makeDoc());
expect(fp).toMatch(/^[0-9a-f]{16}$/);
});
it('is deterministic', () => {
const doc = makeDoc();
expect(generateFingerprint(doc)).toBe(generateFingerprint(doc));
});
it('same error from different sessions produces same fingerprint', () => {
const fp1 = generateFingerprint(makeDoc({ sessionId: 'a' }));
const fp2 = generateFingerprint(makeDoc({ sessionId: 'b' }));
expect(fp1).toBe(fp2);
});
it('same error from different versions produces same fingerprint', () => {
const fp1 = generateFingerprint(makeDoc({ appVersion: '1.0.0' }));
const fp2 = generateFingerprint(makeDoc({ appVersion: '1.1.0' }));
expect(fp1).toBe(fp2);
});
it('different error name produces different fingerprint', () => {
const fp1 = generateFingerprint(makeDoc({ eventName: 'recognition_failed' }));
const fp2 = generateFingerprint(makeDoc({ eventName: 'recognition_timeout' }));
expect(fp1).not.toBe(fp2);
});
it('different module produces different fingerprint', () => {
const fp1 = generateFingerprint(makeDoc({ module: 'dictation' }));
const fp2 = generateFingerprint(makeDoc({ module: 'audio' }));
expect(fp1).not.toBe(fp2);
});
it('different errorDomain produces different fingerprint', () => {
const fp1 = generateFingerprint(makeDoc({ errorDomain: 'kAFAssistant' }));
const fp2 = generateFingerprint(makeDoc({ errorDomain: 'NSURLError' }));
expect(fp1).not.toBe(fp2);
});
it('normalizes numeric differences in messages', () => {
const fp1 = generateFingerprint(makeDoc({ message: 'Timeout after 5000ms' }));
const fp2 = generateFingerprint(makeDoc({ message: 'Timeout after 3000ms' }));
expect(fp1).toBe(fp2);
});
});
// ─── policyMatchesContext ───────────────────────────────────────────
describe('policyMatchesContext', () => {
function makePolicy(
overrides: Partial<TelemetryCollectionPolicyDoc> = {}
): TelemetryCollectionPolicyDoc {
return {
id: 'pol_1',
productId: 'lysnrai',
name: 'Test',
description: '',
enabled: true,
priority: 50,
eventTypes: ['error', 'fatal'],
modules: [],
samplingRate: 1.0,
targeting: {},
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
createdBy: 'admin',
...overrides,
};
}
it('matches when targeting is empty (matches all)', () => {
expect(policyMatchesContext(makePolicy(), { platform: 'ios' })).toBe(true);
});
it('matches platform targeting', () => {
const policy = makePolicy({ targeting: { platforms: ['ios'] } });
expect(policyMatchesContext(policy, { platform: 'ios' })).toBe(true);
expect(policyMatchesContext(policy, { platform: 'android' })).toBe(false);
});
it('matches channel targeting', () => {
const policy = makePolicy({ targeting: { channels: ['keyboard_extension'] } });
expect(policyMatchesContext(policy, { channel: 'keyboard_extension' })).toBe(true);
expect(policyMatchesContext(policy, { channel: 'mobile_app' })).toBe(false);
});
it('matches osFamily targeting', () => {
const policy = makePolicy({ targeting: { osFamilies: ['ios', 'macos'] } });
expect(policyMatchesContext(policy, { osFamily: 'ios' })).toBe(true);
expect(policyMatchesContext(policy, { osFamily: 'android' })).toBe(false);
});
it('matches userId targeting', () => {
const policy = makePolicy({ targeting: { userIds: ['usr_abc'] } });
expect(policyMatchesContext(policy, { userId: 'usr_abc' })).toBe(true);
expect(policyMatchesContext(policy, { userId: 'usr_xyz' })).toBe(false);
});
it('matches appVersion targeting', () => {
const policy = makePolicy({ targeting: { appVersions: ['1.0.0', '1.1.0'] } });
expect(policyMatchesContext(policy, { appVersion: '1.0.0' })).toBe(true);
expect(policyMatchesContext(policy, { appVersion: '2.0.0' })).toBe(false);
});
it('matches buildNumber range targeting', () => {
const policy = makePolicy({ targeting: { buildNumberRange: { min: 10, max: 50 } } });
expect(policyMatchesContext(policy, { buildNumber: '25' })).toBe(true);
expect(policyMatchesContext(policy, { buildNumber: '5' })).toBe(false);
expect(policyMatchesContext(policy, { buildNumber: '100' })).toBe(false);
});
it('matches releaseChannel targeting', () => {
const policy = makePolicy({ targeting: { releaseChannels: ['beta'] } });
expect(policyMatchesContext(policy, { releaseChannel: 'beta' })).toBe(true);
expect(policyMatchesContext(policy, { releaseChannel: 'prod' })).toBe(false);
});
it('rejects expired policy', () => {
const policy = makePolicy({ expiresAt: '2020-01-01T00:00:00.000Z' });
expect(policyMatchesContext(policy, { platform: 'ios' })).toBe(false);
});
it('rejects future-only policy', () => {
const policy = makePolicy({ startsAt: '2099-01-01T00:00:00.000Z' });
expect(policyMatchesContext(policy, { platform: 'ios' })).toBe(false);
});
it('matches countryCode targeting', () => {
const policy = makePolicy({ targeting: { countryCodes: ['US', 'CA'] } });
expect(policyMatchesContext(policy, { countryCode: 'US' })).toBe(true);
expect(policyMatchesContext(policy, { countryCode: 'DE' })).toBe(false);
});
it('matches regionCode targeting', () => {
const policy = makePolicy({ targeting: { regionCodes: ['US:WA', 'US:CA'] } });
expect(policyMatchesContext(policy, { regionCode: 'US:WA' })).toBe(true);
expect(policyMatchesContext(policy, { regionCode: 'US:TX' })).toBe(false);
});
});
// ─── mergePolicies ──────────────────────────────────────────────────
describe('mergePolicies', () => {
function makePolicy(
overrides: Partial<TelemetryCollectionPolicyDoc> = {}
): TelemetryCollectionPolicyDoc {
return {
id: 'pol_1',
productId: 'lysnrai',
name: 'Test',
description: '',
enabled: true,
priority: 50,
eventTypes: ['error', 'fatal'],
modules: [],
samplingRate: 1.0,
targeting: {},
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
createdBy: 'admin',
...overrides,
};
}
it('returns defaults when no policies', () => {
const config = mergePolicies([]);
expect(config.enabled).toBe(true);
expect(config.eventTypes).toEqual(['warn', 'error', 'fatal']);
expect(config.samplingRates.error).toBe(1);
expect(config.samplingRates.debug).toBe(0);
});
it('unions event types from multiple policies', () => {
const config = mergePolicies([
makePolicy({ eventTypes: ['error'], samplingRate: 1.0 }),
makePolicy({ id: 'pol_2', eventTypes: ['debug', 'info'], samplingRate: 0.5 }),
]);
expect(config.eventTypes).toContain('error');
expect(config.eventTypes).toContain('debug');
expect(config.eventTypes).toContain('info');
});
it('first policy sampling rate wins per event type', () => {
const config = mergePolicies([
makePolicy({ eventTypes: ['error'], samplingRate: 0.8 }),
makePolicy({ id: 'pol_2', eventTypes: ['error'], samplingRate: 0.2 }),
]);
expect(config.samplingRates.error).toBe(0.8);
});
it('priority 999 + samplingRate 0 disables everything', () => {
const config = mergePolicies([
makePolicy({ priority: 999, samplingRate: 0, eventTypes: ['error'] }),
]);
expect(config.enabled).toBe(false);
});
it('unions modules from multiple policies', () => {
const config = mergePolicies([
makePolicy({ modules: ['dictation'] }),
makePolicy({ id: 'pol_2', modules: ['audio', 'dictation'] }),
]);
expect(config.modules).toContain('dictation');
expect(config.modules).toContain('audio');
expect(config.modules.length).toBe(2);
});
});