meta-capi-param-builder-clientjs
Version:
Conversions API parameter builder for Client-side JavaScript
879 lines (791 loc) • 26.2 kB
JavaScript
/*
* 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 { getNormalizedName } from '../utils/piiUtil/stringUtil.js';
import { PII_DATA_TYPE } from '../model/constants.js';
describe('getNormalizedName', () => {
describe('Input validation', () => {
it('should return null for null input', () => {
expect(getNormalizedName(null)).toBeNull();
});
it('should return null for undefined input', () => {
expect(getNormalizedName(undefined)).toBeNull();
});
it('should convert non-string inputs to strings and normalize', () => {
const testCases = [
{
input: 123,
expected: '123', // Number converted to string, no punctuation to strip
},
{
input: true,
expected: 'true', // Boolean converted to string
},
{
input: false,
expected: 'false', // Boolean converted to string
},
{
input: {},
expected: 'objectobject', // Object toString becomes '[object Object]', brackets and spaces stripped
},
{
input: [],
expected: '', // Empty array toString is empty string
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle empty string input', () => {
expect(getNormalizedName('')).toEqual(''); // Empty string after normalization
});
it('should handle whitespace-only input', () => {
expect(getNormalizedName(' ')).toEqual(''); // Whitespace stripped
expect(getNormalizedName('\t\n\r')).toEqual(''); // Whitespace stripped
});
});
describe('Name normalization', () => {
it('should handle example data', () => {
const testCases = [
{
input: 'John',
expected: 'john',
},
{
input: " Na'than-Boile ",
expected: 'nathanboile',
},
{
input: '정',
expected: '정',
},
{
input: 'Valéry',
expected: 'valéry',
},
{
input: 'Doe',
expected: 'doe', // Apostrophe stripped
},
{
input: ' Doe-Doe ',
expected: 'doedoe', // Spaces stripped
},
{
input: 'Jean-Pierre',
expected: 'jeanpierre', // Hyphen stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should normalize basic names', () => {
const testCases = [
{
input: 'John',
expected: 'john',
},
{
input: 'MARY',
expected: 'mary',
},
{
input: 'smith',
expected: 'smith',
},
{
input: 'McDonald',
expected: 'mcdonald',
},
{
input: "O'Connor",
expected: 'oconnor', // Apostrophe stripped
},
{
input: 'Van Der Berg',
expected: 'vanderberg', // Spaces stripped
},
{
input: 'Jean-Pierre',
expected: 'jeanpierre', // Hyphen stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with various punctuation', () => {
const testCases = [
{
input: 'John.',
expected: 'john', // Period stripped
},
{
input: 'Mary,',
expected: 'mary', // Comma stripped
},
{
input: 'Dr. Smith',
expected: 'drsmith', // Period and space stripped
},
{
input: 'John Jr.',
expected: 'johnjr', // Period and space stripped
},
{
input: 'Mary-Jane',
expected: 'maryjane', // Hyphen stripped
},
{
input: "O'Reilly",
expected: 'oreilly', // Apostrophe stripped
},
{
input: 'João',
expected: 'joão', // Accented characters preserved, lowercased
},
{
input: 'José',
expected: 'josé', // Accented characters preserved, lowercased
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with leading/trailing whitespace', () => {
const testCases = [
{
input: ' John ',
expected: 'john', // Whitespace stripped
},
{
input: '\tMary\n',
expected: 'mary', // Whitespace stripped
},
{
input: ' Dr. Smith ',
expected: 'drsmith', // All whitespace and punctuation stripped
},
{
input: '\r\nJohn Jr.\t',
expected: 'johnjr', // All whitespace and punctuation stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with mixed case', () => {
const testCases = [
{
input: 'JoHn',
expected: 'john',
},
{
input: 'mArY',
expected: 'mary',
},
{
input: 'SmItH',
expected: 'smith',
},
{
input: 'McDONALD',
expected: 'mcdonald',
},
{
input: 'VAN DER BERG',
expected: 'vanderberg',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with numbers', () => {
const testCases = [
{
input: 'John2',
expected: 'john2',
},
{
input: 'Mary123',
expected: 'mary123',
},
{
input: '3rd John',
expected: '3rdjohn', // Space stripped
},
{
input: 'Agent007',
expected: 'agent007',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with special characters and symbols', () => {
const testCases = [
{
input: 'John!',
expected: 'john', // Exclamation mark stripped
},
{
input: 'Mary@Domain',
expected: 'marydomain', // @ symbol stripped
},
{
input: 'Smith#123',
expected: 'smith123', // # symbol stripped
},
{
input: 'John$Dollar',
expected: 'johndollar', // $ symbol stripped
},
{
input: 'Mary%Percent',
expected: 'marypercent', // % symbol stripped
},
{
input: 'John&Mary',
expected: 'johnmary', // & symbol stripped
},
{
input: 'Smith*Star',
expected: 'smithstar', // * symbol stripped
},
{
input: 'John+Plus',
expected: 'johnplus', // + symbol stripped
},
{
input: 'Mary=Equal',
expected: 'maryequal', // = symbol stripped
},
{
input: 'John?Question',
expected: 'johnquestion', // ? symbol stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with brackets and braces', () => {
const testCases = [
{
input: 'John(Junior)',
expected: 'johnjunior', // Parentheses stripped
},
{
input: 'Mary[Smith]',
expected: 'marysmith', // Square brackets stripped
},
{
input: 'John{Test}',
expected: 'johntest', // Curly braces stripped
},
{
input: '[Dr.] Smith',
expected: 'drsmith', // Brackets and punctuation stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle names with colons, semicolons, and pipes', () => {
const testCases = [
{
input: 'John:Smith',
expected: 'johnsmith', // Colon stripped
},
{
input: 'Mary;Jane',
expected: 'maryjane', // Semicolon stripped
},
{
input: 'John|Smith',
expected: 'johnsmith', // Pipe stripped
},
{
input: 'Title: John Smith',
expected: 'titlejohnsmith', // Colon and space stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle very long names', () => {
const longName = 'A'.repeat(100) + ' B'.repeat(50);
const result = getNormalizedName(longName);
expect(result).toEqual('a'.repeat(100) + 'b'.repeat(50)); // Spaces stripped, lowercased
});
it('should handle names with only punctuation', () => {
const testCases = [
{
input: '...',
expected: '', // All punctuation stripped
},
{
input: '!!!',
expected: '', // All punctuation stripped
},
{
input: '---',
expected: '', // All punctuation stripped
},
{
input: '(),[]{}',
expected: '', // All punctuation stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle international names', () => {
const testCases = [
{
input: 'José',
expected: 'josé',
},
{
input: 'François',
expected: 'françois',
},
{
input: 'Müller',
expected: 'müller',
},
{
input: 'Søren',
expected: 'søren',
},
{
input: 'Αθήνα', // Greek
expected: 'αθήνα',
},
{
input: 'Москва', // Russian
expected: 'москва',
},
{
input: '北京', // Chinese
expected: '北京',
},
{
input: '東京', // Japanese
expected: '東京',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
});
describe('Hashed name 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 = getNormalizedName(hash);
expect(result).toEqual(hash);
});
});
it('should process invalid hash-like strings as regular names', () => {
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', // ! stripped
},
{
input: '123', // too short for hash
expected: '123',
},
];
invalidHashes.forEach(({ input, expected }) => {
const result = getNormalizedName(input);
expect(result).toEqual(expected);
});
});
it('should handle mixed case hashed names', () => {
const mixedCaseHash =
'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3';
const result = getNormalizedName(mixedCaseHash);
expect(result).toEqual(mixedCaseHash);
});
it('should handle lowercase hashed names', () => {
const lowercaseHash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
const result = getNormalizedName(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: '456789', // Decimal point stripped
},
{
input: '-123',
expected: '123', // Minus sign stripped
},
{
input: '+456',
expected: '456', // Plus sign stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(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 punctuation stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
it('should handle Unicode and emoji', () => {
const testCases = [
{
input: 'John 😀',
expected: 'john😀', // Space stripped, emoji preserved
},
{
input: 'Mary ❤️',
expected: 'mary❤️', // Space stripped, emoji preserved
},
{
input: '🎉 Party',
expected: '🎉party', // Space stripped, emoji preserved
},
{
input: '名前', // Japanese characters
expected: '名前',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
});
});
});
});
describe('getNormalizedPII with FIRST_NAME', () => {
describe('Input validation', () => {
it('should return null for null input', () => {
expect(getNormalizedPII(null, PII_DATA_TYPE.FIRST_NAME)).toBeNull();
});
it('should return null for undefined input', () => {
expect(getNormalizedPII(undefined, PII_DATA_TYPE.FIRST_NAME)).toBeNull();
});
it('should return null for non-string input', () => {
const nonStringValues = [123, {}, [], true, false];
nonStringValues.forEach((value) => {
expect(getNormalizedPII(value, PII_DATA_TYPE.FIRST_NAME)).toBeNull();
});
});
it('should return null for empty string input', () => {
expect(getNormalizedPII('', PII_DATA_TYPE.FIRST_NAME)).toBeNull();
});
it('should return empty string for whitespace-only input', () => {
expect(getNormalizedPII(' ', PII_DATA_TYPE.FIRST_NAME)).toEqual('');
expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.FIRST_NAME)).toEqual('');
});
it('should return null for null data type', () => {
expect(getNormalizedPII('John', null)).toBeNull();
});
it('should return null for undefined data type', () => {
expect(getNormalizedPII('John', undefined)).toBeNull();
});
it('should return null for invalid data type', () => {
expect(getNormalizedPII('John', 'invalid_type')).toBeNull();
});
});
describe('First name normalization', () => {
it('should normalize first names using getNormalizedName', () => {
const testCases = [
{
input: 'John',
expected: 'john',
},
{
input: 'MARY',
expected: 'mary',
},
{
input: ' Dr. Smith ',
expected: 'drsmith',
},
{
input: "O'Connor",
expected: 'oconnor',
},
{
input: 'Jean-Pierre',
expected: 'jeanpierre',
},
{
input: 'José',
expected: 'josé',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedPII(input, PII_DATA_TYPE.FIRST_NAME)).toEqual(
expected
);
});
});
it('should handle hashed first names', () => {
const hash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
expect(getNormalizedPII(hash, PII_DATA_TYPE.FIRST_NAME)).toEqual(hash);
});
it('should handle whitespace-only first names', () => {
// PII interface passes whitespace through to getNormalizedName which strips it to empty string
expect(getNormalizedPII(' ', PII_DATA_TYPE.FIRST_NAME)).toEqual('');
expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.FIRST_NAME)).toEqual('');
});
});
describe('Other PII data types', () => {
it('should return null for invalid email when using EMAIL data type', () => {
expect(getNormalizedPII('John', PII_DATA_TYPE.EMAIL)).toBeNull();
expect(getNormalizedPII('Mary', PII_DATA_TYPE.EMAIL)).toBeNull();
});
it('should return normalized phone for name when using PHONE data type', () => {
expect(getNormalizedPII('John', PII_DATA_TYPE.PHONE)).toEqual('');
expect(getNormalizedPII('Mary123', PII_DATA_TYPE.PHONE)).toEqual('123');
expect(getNormalizedPII('123-456-7890', PII_DATA_TYPE.PHONE)).toEqual(
'1234567890'
);
});
it('should return normalized DOB for name when using DATE_OF_BIRTH data type', () => {
expect(getNormalizedPII('John', PII_DATA_TYPE.DATE_OF_BIRTH)).toBeNull(); // Invalid date
expect(getNormalizedPII('Mary', 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 name 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('John', PII_DATA_TYPE.GENDER)).toBeNull(); // Invalid gender term
});
it('should return null for truly unsupported data types', () => {
const unsupportedTypes = ['invalid_type', 'unknown', 'fake_type'];
unsupportedTypes.forEach((type) => {
expect(getNormalizedPII('John', type)).toBeNull();
});
});
});
});
describe('getNormalizedPII with LAST_NAME', () => {
describe('Input validation', () => {
it('should return null for null input', () => {
expect(getNormalizedPII(null, PII_DATA_TYPE.LAST_NAME)).toBeNull();
});
it('should return null for undefined input', () => {
expect(getNormalizedPII(undefined, PII_DATA_TYPE.LAST_NAME)).toBeNull();
});
it('should return null for non-string input', () => {
const nonStringValues = [123, {}, [], true, false];
nonStringValues.forEach((value) => {
expect(getNormalizedPII(value, PII_DATA_TYPE.LAST_NAME)).toBeNull();
});
});
it('should return null for empty string input', () => {
expect(getNormalizedPII('', PII_DATA_TYPE.LAST_NAME)).toBeNull();
});
it('should return empty string for whitespace-only input', () => {
expect(getNormalizedPII(' ', PII_DATA_TYPE.LAST_NAME)).toEqual('');
expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.LAST_NAME)).toEqual('');
});
it('should return null for null data type', () => {
expect(getNormalizedPII('Smith', null)).toBeNull();
});
it('should return null for undefined data type', () => {
expect(getNormalizedPII('Smith', undefined)).toBeNull();
});
it('should return null for invalid data type', () => {
expect(getNormalizedPII('Smith', 'invalid_type')).toBeNull();
});
});
describe('Last name normalization', () => {
it('should normalize last names using getNormalizedName', () => {
const testCases = [
{
input: 'Smith',
expected: 'smith',
},
{
input: 'JOHNSON',
expected: 'johnson',
},
{
input: ' McDonald ',
expected: 'mcdonald',
},
{
input: "O'Reilly",
expected: 'oreilly',
},
{
input: 'Van Der Berg',
expected: 'vanderberg',
},
{
input: 'Müller',
expected: 'müller',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedPII(input, PII_DATA_TYPE.LAST_NAME)).toEqual(
expected
);
});
});
it('should handle hashed last names', () => {
const hash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
expect(getNormalizedPII(hash, PII_DATA_TYPE.LAST_NAME)).toEqual(hash);
});
it('should handle whitespace-only last names', () => {
// PII interface passes whitespace through to getNormalizedName which strips it to empty string
expect(getNormalizedPII(' ', PII_DATA_TYPE.LAST_NAME)).toEqual('');
expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.LAST_NAME)).toEqual('');
});
});
describe('Comparison with direct getNormalizedName', () => {
it('should show both functions behave the same way for valid names', () => {
const testCases = [
{
input: 'Smith',
expected: 'smith',
},
{
input: ' Dr. Johnson ',
expected: 'drjohnson',
},
{
input: "O'Connor",
expected: 'oconnor',
},
{
input: 'Van Der Berg',
expected: 'vanderberg',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedName(input)).toEqual(expected);
expect(getNormalizedPII(input, PII_DATA_TYPE.LAST_NAME)).toEqual(
expected
);
expect(getNormalizedPII(input, PII_DATA_TYPE.FIRST_NAME)).toEqual(
expected
);
});
});
it('should show both handle hashes the same way', () => {
const hash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
expect(getNormalizedName(hash)).toEqual(hash);
expect(getNormalizedPII(hash, PII_DATA_TYPE.LAST_NAME)).toEqual(hash);
expect(getNormalizedPII(hash, PII_DATA_TYPE.FIRST_NAME)).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(getNormalizedName(input)).toBeNull();
// PII interface also returns null for invalid inputs (validates input first)
expect(getNormalizedPII(input, PII_DATA_TYPE.LAST_NAME)).toBeNull();
expect(getNormalizedPII(input, PII_DATA_TYPE.FIRST_NAME)).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 = getNormalizedName(input);
expect(typeof directResult).toBe('string');
// PII interface returns null for non-string inputs (validates input first)
expect(getNormalizedPII(input, PII_DATA_TYPE.LAST_NAME)).toBeNull();
expect(getNormalizedPII(input, PII_DATA_TYPE.FIRST_NAME)).toBeNull();
});
});
it('should show different handling for whitespace-only strings', () => {
const whitespaceInputs = [' ', '\t\n\r'];
whitespaceInputs.forEach((input) => {
// Direct function processes whitespace (strips it, resulting in empty string)
const directResult = getNormalizedName(input);
expect(directResult).toEqual('');
// PII interface passes through to getNormalizedName (which strips to empty string)
expect(getNormalizedPII(input, PII_DATA_TYPE.LAST_NAME)).toEqual('');
expect(getNormalizedPII(input, PII_DATA_TYPE.FIRST_NAME)).toEqual('');
});
// Handle empty string separately since PII interface validates it differently
expect(getNormalizedName('')).toEqual('');
expect(getNormalizedPII('', PII_DATA_TYPE.LAST_NAME)).toBeNull();
expect(getNormalizedPII('', PII_DATA_TYPE.FIRST_NAME)).toBeNull();
});
});
});