test(telemetry): add route-logic tests — containsPII, computePk, normalizeMessage, fingerprint, policyMatch, mergePolicies (34→77 tests)
This commit is contained in:
parent
dcc0befb8c
commit
20f77d5a50
@ -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 {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user