UNPKG

meta-capi-param-builder-clientjs

Version:

Conversions API parameter builder for Client-side JavaScript

782 lines (704 loc) 24.1 kB
/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import { getNormalizedPII } from '../utils/piiUtil/piiUtil.js'; import { getNormalizedExternalID } from '../utils/piiUtil/stringUtil.js'; import { PII_DATA_TYPE } from '../model/constants.js'; describe('getNormalizedExternalID', () => { describe('Input validation', () => { it('should return null for null input', () => { expect(getNormalizedExternalID(null)).toBeNull(); }); it('should return null for undefined input', () => { expect(getNormalizedExternalID(undefined)).toBeNull(); }); it('should convert non-string inputs to strings and normalize', () => { const testCases = [ { input: 123, expected: '123', }, { input: true, expected: 'true', }, { input: false, expected: 'false', }, { input: {}, expected: '[objectobject]', // Object toString becomes '[object Object]', spaces stripped, lowercased, brackets preserved }, { input: [], expected: '', // Empty array toString is empty string }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle empty string input', () => { expect(getNormalizedExternalID('')).toEqual(''); }); it('should handle whitespace-only input', () => { expect(getNormalizedExternalID(' ')).toEqual(''); // Whitespace stripped expect(getNormalizedExternalID('\t\n\r')).toEqual(''); // Whitespace stripped }); }); describe('External ID normalization', () => { it('should normalize example data', () => { const testCases = [ { input: ' 12345', expected: '12345', }, { input: 'abc-12345 ', expected: 'abc-12345', // Lowercased }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should normalize basic external IDs', () => { const testCases = [ { input: 'user123', expected: 'user123', }, { input: 'USER456', expected: 'user456', // Lowercased }, { input: 'Customer_789', expected: 'customer_789', // Lowercased, underscore preserved }, { input: 'ext-id-001', expected: 'ext-id-001', // Lowercased, hyphens preserved }, { input: 'ID.2023.ABC', expected: 'id.2023.abc', // Lowercased, dots preserved }, { input: 'ref#12345', expected: 'ref#12345', // Lowercased, special chars preserved }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should strip whitespace but preserve other characters', () => { const testCases = [ { input: ' user123 ', expected: 'user123', // Leading/trailing whitespace stripped }, { input: '\tUSER456\n', expected: 'user456', // Tab/newline whitespace stripped, lowercased }, { input: ' Customer_789 ', expected: 'customer_789', // Whitespace stripped, underscore preserved }, { input: '\r\next-id-001\t', expected: 'ext-id-001', // All whitespace stripped }, { input: 'user 123 id', expected: 'user123id', // Internal spaces stripped }, { input: 'ID\t2023\nABC', expected: 'id2023abc', // Internal whitespace stripped }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle mixed case external IDs', () => { const testCases = [ { input: 'UsEr123', expected: 'user123', }, { input: 'CuStOmEr_456', expected: 'customer_456', }, { input: 'EXT-ID-001', expected: 'ext-id-001', }, { input: 'ID.MiXeD.CaSe', expected: 'id.mixed.case', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should preserve special characters and punctuation', () => { const testCases = [ { input: 'user@domain.com', expected: 'user@domain.com', }, { input: 'ID#123-ABC', expected: 'id#123-abc', }, { input: 'ext_id_2023', expected: 'ext_id_2023', }, { input: 'customer.ref.001', expected: 'customer.ref.001', }, { input: 'user|123|test', expected: 'user|123|test', }, { input: 'id[456]', expected: 'id[456]', }, { input: 'ref{789}', expected: 'ref{789}', }, { input: 'user+premium', expected: 'user+premium', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle numeric external IDs', () => { const testCases = [ { input: '12345', expected: '12345', }, { input: '0001', expected: '0001', }, { input: '999999999', expected: '999999999', }, { input: '0', expected: '0', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle alphanumeric external IDs', () => { const testCases = [ { input: 'user123abc', expected: 'user123abc', }, { input: 'ABC123DEF', expected: 'abc123def', }, { input: '123abc456def', expected: '123abc456def', }, { input: 'a1b2c3d4', expected: 'a1b2c3d4', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle UUIDs and GUID-like formats', () => { const testCases = [ { input: '550e8400-e29b-41d4-a716-446655440000', expected: '550e8400-e29b-41d4-a716-446655440000', }, { input: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', expected: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', }, { input: 'F47AC10B-58CC-4372-A567-0E02B2C3D479', expected: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', // Lowercased }, { input: '{6ba7b814-9dad-11d1-80b4-00c04fd430c8}', expected: '{6ba7b814-9dad-11d1-80b4-00c04fd430c8}', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle very long external IDs', () => { const longId = 'A'.repeat(100) + 'B'.repeat(50); const result = getNormalizedExternalID(longId); expect(result).toEqual('a'.repeat(100) + 'b'.repeat(50)); // Lowercased }); it('should handle external IDs with only special characters', () => { const testCases = [ { input: '!@#$%^&*()', expected: '!@#$%^&*()', }, { input: '[]{}|;:,.<>?', expected: '[]{}|;:,.<>?', }, { input: '---', expected: '---', }, { input: '...', expected: '...', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle international and Unicode characters', () => { const testCases = [ { input: 'José123', expected: 'josé123', }, { input: 'François456', expected: 'françois456', }, { input: 'Müller789', expected: 'müller789', }, { input: 'Søren001', expected: 'søren001', }, { input: 'Αθήνα123', // Greek expected: 'αθήνα123', }, { input: 'Москва456', // Russian expected: 'москва456', }, { input: '北京789', // Chinese expected: '北京789', }, { input: '東京001', // Japanese expected: '東京001', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle external IDs with emoji', () => { const testCases = [ { input: 'user123😀', expected: 'user123😀', }, { input: 'ID❤️456', expected: 'id❤️456', }, { input: '🎉party789', expected: '🎉party789', }, { input: '😀ID001', expected: '😀id001', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); }); describe('Hashed external ID handling', () => { it('should return SHA-256 hash as-is when it looks like a hash', () => { const validHashes = [ 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', // 'hello' hashed 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', // empty string hashed '2cf24dba4f21d4288094e8452703c0f0142fa00b2eeb1f2c9b4e70f39e8a4c29', // 'hello' in different case 'ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890', // uppercase hash '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', // mixed case hash ]; validHashes.forEach((hash) => { const result = getNormalizedExternalID(hash); expect(result).toEqual(hash); }); }); it('should process invalid hash-like strings as regular external IDs', () => { const invalidHashes = [ { input: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae', // 63 chars expected: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae', }, { input: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae33', // 65 chars expected: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae33', }, { input: 'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', // invalid char 'g' expected: 'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', }, { input: 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE!', // invalid char '!' expected: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae!', // ! preserved, lowercased }, { input: '123', // too short for hash expected: '123', }, ]; invalidHashes.forEach(({ input, expected }) => { const result = getNormalizedExternalID(input); expect(result).toEqual(expected); }); }); it('should handle mixed case hashed external IDs', () => { const mixedCaseHash = 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3'; const result = getNormalizedExternalID(mixedCaseHash); expect(result).toEqual(mixedCaseHash); }); it('should handle lowercase hashed external IDs', () => { const lowercaseHash = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'; const result = getNormalizedExternalID(lowercaseHash); expect(result).toEqual(lowercaseHash); }); }); describe('Edge cases', () => { it('should handle numeric strings', () => { const testCases = [ { input: '123', expected: '123', }, { input: '0', expected: '0', }, { input: '456.789', expected: '456.789', // Decimal point preserved }, { input: '-123', expected: '-123', // Minus sign preserved }, { input: '+456', expected: '+456', // Plus sign preserved }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle extreme cases', () => { const testCases = [ { input: String(Number.MAX_SAFE_INTEGER), expected: String(Number.MAX_SAFE_INTEGER).toLowerCase(), }, { input: 'A'.repeat(1000), // Very long string expected: 'a'.repeat(1000), }, { input: '!@#$%^&*()_+-=[]{}|;:,.<>?', expected: '!@#$%^&*()_+-=[]{}|;:,.<>?', // All special chars preserved }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); it('should handle common external ID formats', () => { const testCases = [ { input: 'cust_1234567890', expected: 'cust_1234567890', // Stripe-style customer ID }, { input: 'acct_0123456789', expected: 'acct_0123456789', // Stripe-style account ID }, { input: 'usr-ABC123DEF456', expected: 'usr-abc123def456', // Custom format with hyphens }, { input: 'REF.2023.001.ABC', expected: 'ref.2023.001.abc', // Reference number format }, { input: 'ID:12345:ABC:789', expected: 'id:12345:abc:789', // Colon-separated format }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); }); }); }); }); describe('getNormalizedPII with EXTERNAL_ID', () => { describe('Input validation', () => { it('should return null for null input', () => { expect(getNormalizedPII(null, PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); it('should return null for undefined input', () => { expect(getNormalizedPII(undefined, PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); it('should return null for non-string input', () => { const nonStringValues = [123, {}, [], true, false]; nonStringValues.forEach((value) => { expect(getNormalizedPII(value, PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); }); it('should return null for empty string input', () => { expect(getNormalizedPII('', PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); it('should return empty string for whitespace-only input', () => { expect(getNormalizedPII(' ', PII_DATA_TYPE.EXTERNAL_ID)).toEqual(''); expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.EXTERNAL_ID)).toEqual(''); }); it('should return null for null data type', () => { expect(getNormalizedPII('user123', null)).toBeNull(); }); it('should return null for undefined data type', () => { expect(getNormalizedPII('user123', undefined)).toBeNull(); }); it('should return null for invalid data type', () => { expect(getNormalizedPII('user123', 'invalid_type')).toBeNull(); }); }); describe('External ID normalization', () => { it('should normalize external IDs using getNormalizedExternalID', () => { const testCases = [ { input: 'USER123', expected: 'user123', }, { input: 'Customer_456', expected: 'customer_456', }, { input: ' ext-id-001 ', expected: 'ext-id-001', }, { input: 'ID.2023.ABC', expected: 'id.2023.abc', }, { input: 'ref#12345', expected: 'ref#12345', }, { input: '550e8400-e29b-41d4-a716-446655440000', expected: '550e8400-e29b-41d4-a716-446655440000', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedPII(input, PII_DATA_TYPE.EXTERNAL_ID)).toEqual( expected ); }); }); it('should handle hashed external IDs', () => { const hash = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'; expect(getNormalizedPII(hash, PII_DATA_TYPE.EXTERNAL_ID)).toEqual(hash); }); it('should return empty string for whitespace-only input after PII validation', () => { // PII interface passes through to getNormalizedExternalID for EXTERNAL_ID type expect(getNormalizedPII(' ', PII_DATA_TYPE.EXTERNAL_ID)).toEqual(''); expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.EXTERNAL_ID)).toEqual(''); }); }); describe('Other PII data types', () => { it('should return null for invalid email when using EMAIL data type', () => { expect(getNormalizedPII('user123', PII_DATA_TYPE.EMAIL)).toBeNull(); expect(getNormalizedPII('ext-id-001', PII_DATA_TYPE.EMAIL)).toBeNull(); }); it('should return normalized phone for external ID when using PHONE data type', () => { expect(getNormalizedPII('user123', PII_DATA_TYPE.PHONE)).toEqual('123'); expect(getNormalizedPII('ext-id-001', PII_DATA_TYPE.PHONE)).toEqual( '1' // Leading zeros stripped ); expect(getNormalizedPII('123-456-7890', PII_DATA_TYPE.PHONE)).toEqual( '1234567890' ); }); it('should return normalized DOB for external ID when using DATE_OF_BIRTH data type', () => { expect( getNormalizedPII('user123', PII_DATA_TYPE.DATE_OF_BIRTH) ).toBeNull(); // Invalid date expect( getNormalizedPII('ext-id-001', PII_DATA_TYPE.DATE_OF_BIRTH) ).toBeNull(); // Invalid date expect( getNormalizedPII('12-31-1990', PII_DATA_TYPE.DATE_OF_BIRTH) ).toEqual('19901231'); }); it('should return normalized gender for external ID when using GENDER data type', () => { expect(getNormalizedPII('male', PII_DATA_TYPE.GENDER)).toEqual('m'); expect(getNormalizedPII('female', PII_DATA_TYPE.GENDER)).toEqual('f'); expect(getNormalizedPII('user123', PII_DATA_TYPE.GENDER)).toBeNull(); // Invalid gender term }); it('should return normalized name for external ID when using NAME data types', () => { expect(getNormalizedPII('user123', PII_DATA_TYPE.FIRST_NAME)).toEqual( 'user123' ); expect(getNormalizedPII('ext-id-001', PII_DATA_TYPE.LAST_NAME)).toEqual( 'extid001' ); expect( getNormalizedPII('Customer_456', PII_DATA_TYPE.FIRST_NAME) ).toEqual('customer456'); }); it('should return normalized city for external ID when using CITY data type', () => { expect(getNormalizedPII('user123', PII_DATA_TYPE.CITY)).toEqual( 'user123' ); expect(getNormalizedPII('ext-id-001', PII_DATA_TYPE.CITY)).toEqual( 'extid001' ); expect(getNormalizedPII('123user', PII_DATA_TYPE.CITY)).toBeNull(); // Starts with number }); it('should return normalized state for external ID when using STATE data type', () => { expect(getNormalizedPII('user123', PII_DATA_TYPE.STATE)).toEqual('us'); // Truncated to 2 chars expect(getNormalizedPII('ext-id-001', PII_DATA_TYPE.STATE)).toEqual('ex'); // Truncated to 2 chars expect(getNormalizedPII('California', PII_DATA_TYPE.STATE)).toEqual('ca'); // State match }); it('should return normalized country for external ID when using COUNTRY data type', () => { expect(getNormalizedPII('user123', PII_DATA_TYPE.COUNTRY)).toEqual('us'); // Truncated to 2 chars expect(getNormalizedPII('ext-id-001', PII_DATA_TYPE.COUNTRY)).toEqual( 'ex' ); // Truncated to 2 chars expect(getNormalizedPII('Germany', PII_DATA_TYPE.COUNTRY)).toEqual('de'); // Country match }); it('should return null for truly unsupported data types', () => { const unsupportedTypes = ['invalid_type', 'unknown', 'fake_type']; unsupportedTypes.forEach((type) => { expect(getNormalizedPII('user123', type)).toBeNull(); }); }); }); describe('Comparison with direct getNormalizedExternalID', () => { it('should show both functions behave the same way for valid external IDs', () => { const testCases = [ { input: 'USER123', expected: 'user123', }, { input: ' Customer_456 ', expected: 'customer_456', }, { input: 'ext-id-001', expected: 'ext-id-001', }, { input: 'ID.2023.ABC', expected: 'id.2023.abc', }, ]; testCases.forEach(({ input, expected }) => { expect(getNormalizedExternalID(input)).toEqual(expected); expect(getNormalizedPII(input, PII_DATA_TYPE.EXTERNAL_ID)).toEqual( expected ); }); }); it('should show both handle hashes the same way', () => { const hash = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'; expect(getNormalizedExternalID(hash)).toEqual(hash); expect(getNormalizedPII(hash, PII_DATA_TYPE.EXTERNAL_ID)).toEqual(hash); }); it('should show different handling for null/invalid inputs due to PII interface validation', () => { const invalidInputs = [null, undefined]; invalidInputs.forEach((input) => { // Direct function returns null for null/undefined expect(getNormalizedExternalID(input)).toBeNull(); // PII interface also returns null for invalid inputs (validates input first) expect(getNormalizedPII(input, PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); }); it('should show different handling for non-string inputs', () => { const nonStringInputs = [123, {}, [], true, false]; nonStringInputs.forEach((input) => { // Direct function converts non-strings to strings and processes them const directResult = getNormalizedExternalID(input); expect(typeof directResult).toBe('string'); // PII interface returns null for non-string inputs (validates input first) expect(getNormalizedPII(input, PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); }); it('should show different handling for whitespace-only strings', () => { const whitespaceInputs = [' ', '\t\n\r']; whitespaceInputs.forEach((input) => { // Direct function processes whitespace (strips it to empty string) const directResult = getNormalizedExternalID(input); expect(directResult).toEqual(''); // PII interface returns empty string for whitespace-only strings for EXTERNAL_ID type expect(getNormalizedPII(input, PII_DATA_TYPE.EXTERNAL_ID)).toEqual(''); }); // Handle empty string separately since PII interface validates it differently expect(getNormalizedExternalID('')).toEqual(''); expect(getNormalizedPII('', PII_DATA_TYPE.EXTERNAL_ID)).toBeNull(); }); }); });