meta-capi-param-builder-clientjs
Version:
Conversions API parameter builder for Client-side JavaScript
817 lines (730 loc) • 25.4 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 { getNormalizedCity } from '../utils/piiUtil/stringUtil.js';
import { PII_DATA_TYPE } from '../model/constants.js';
describe('getNormalizedCity', () => {
describe('Input validation', () => {
it('should return null for null input', () => {
expect(getNormalizedCity(null)).toBeNull();
});
it('should return null for undefined input', () => {
expect(getNormalizedCity(undefined)).toBeNull();
});
it('should convert non-string inputs to strings and normalize', () => {
const testCases = [
{
input: 123,
expected: null, // Starts with number, fails regex test '^[a-z]+'
},
{
input: true,
expected: 'true', // 'true' starts with 't', passes regex test
},
{
input: false,
expected: 'false', // 'false' starts with 'f', passes regex test
},
{
input: {},
expected: 'objectobject', // '[object Object]' becomes 'objectobject', starts with 'o', passes regex test
},
{
input: [],
expected: null, // Empty array toString is empty string, fails regex test
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle empty string input', () => {
expect(getNormalizedCity('')).toBeNull(); // Empty string fails regex test '^[a-z]+'
});
it('should handle whitespace-only input', () => {
expect(getNormalizedCity(' ')).toBeNull(); // Whitespace stripped to empty, fails regex test
expect(getNormalizedCity('\t\n\r')).toBeNull(); // Whitespace stripped to empty, fails regex test
});
});
describe('City normalization', () => {
it('should normalize example data', () => {
const testCases = [
{
input: 'London',
expected: 'london',
},
{
input: 'Menlo Park',
expected: 'menlopark',
},
{
input: ' Menlo-Park ',
expected: 'menlopark',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should normalize basic city names', () => {
const testCases = [
{
input: 'London',
expected: 'london',
},
{
input: 'NEW YORK',
expected: 'newyork',
},
{
input: 'san francisco',
expected: 'sanfrancisco',
},
{
input: 'Los Angeles',
expected: 'losangeles',
},
{
input: 'Chicago',
expected: 'chicago',
},
{
input: 'Boston',
expected: 'boston',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle cities with punctuation and special characters', () => {
const testCases = [
{
input: 'St. Louis',
expected: 'stlouis', // Period and space stripped
},
{
input: 'St-Petersburg',
expected: 'stpetersburg', // Hyphen stripped
},
{
input: "O'Fallon",
expected: 'ofallon', // Apostrophe stripped
},
{
input: 'Washington, D.C.',
expected: 'washingtondc', // Punctuation stripped
},
{
input: 'Miami-Dade',
expected: 'miamidade', // Hyphen stripped
},
{
input: 'Fort Worth',
expected: 'fortworth', // Space stripped
},
{
input: 'Las Vegas',
expected: 'lasvegas', // Space stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle cities with leading/trailing whitespace', () => {
const testCases = [
{
input: ' London ',
expected: 'london', // Whitespace stripped
},
{
input: '\tParis\n',
expected: 'paris', // Whitespace stripped
},
{
input: ' New York ',
expected: 'newyork', // Whitespace and space stripped
},
{
input: '\r\nChicago\t',
expected: 'chicago', // All whitespace stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle cities with mixed case', () => {
const testCases = [
{
input: 'NeW yOrK',
expected: 'newyork',
},
{
input: 'LOS aNGeLes',
expected: 'losangeles',
},
{
input: 'ChIcAgO',
expected: 'chicago',
},
{
input: 'sAn FrAnCiScO',
expected: 'sanfrancisco',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle cities with numbers and alphanumeric characters', () => {
const testCases = [
{
input: 'District9',
expected: 'district9', // Numbers preserved, starts with letter
},
{
input: 'Area51City',
expected: 'area51city', // Numbers preserved, starts with letter
},
{
input: 'Zone2B',
expected: 'zone2b', // Numbers and letters preserved
},
{
input: 'Sector7G',
expected: 'sector7g', // Numbers and letters preserved
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should return null for cities starting with numbers', () => {
const testCases = [
'1stCity', // Starts with number, fails regex test
'2ndDistrict', // Starts with number, fails regex test
'3rdWard', // Starts with number, fails regex test
'42ndStreet', // Starts with number, fails regex test
'99thPrecinct', // Starts with number, fails regex test
];
testCases.forEach((input) => {
expect(getNormalizedCity(input)).toBeNull();
});
});
it('should handle cities with various separators', () => {
const testCases = [
{
input: 'San-Francisco',
expected: 'sanfrancisco', // Hyphen stripped
},
{
input: 'New_York',
expected: 'newyork', // Underscore stripped (non-Latin alpha-numeric)
},
{
input: 'Los/Angeles',
expected: 'losangeles', // Slash stripped
},
{
input: 'Fort|Worth',
expected: 'fortworth', // Pipe stripped
},
{
input: 'Miami@Beach',
expected: 'miamibeach', // @ symbol stripped
},
{
input: 'Salt#Lake#City',
expected: 'saltlakecity', // # symbols stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle international city names with Latin characters', () => {
const testCases = [
{
input: 'São Paulo',
expected: 'sopaulo', // Diacritics stripped but Latin characters preserved
},
{
input: 'México City',
expected: 'mxicocity', // é character completely stripped
},
{
input: 'Zürich',
expected: 'zrich', // Diacritics stripped but Latin characters preserved
},
{
input: 'Montréal',
expected: 'montral', // Diacritics stripped but Latin characters preserved
},
{
input: 'København',
expected: 'kbenhavn', // Diacritics stripped but Latin characters preserved
},
{
input: 'München',
expected: 'mnchen', // Diacritics stripped but Latin characters preserved
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle cities with only Latin alphabet characters', () => {
const testCases = [
{
input: 'London',
expected: 'london',
},
{
input: 'Paris',
expected: 'paris',
},
{
input: 'Berlin',
expected: 'berlin',
},
{
input: 'Madrid',
expected: 'madrid',
},
{
input: 'Rome',
expected: 'rome',
},
{
input: 'Amsterdam',
expected: 'amsterdam',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should return null for cities with only non-Latin characters', () => {
const testCases = [
'北京', // Chinese characters
'東京', // Japanese characters
'Москва', // Russian characters
'Αθήνα', // Greek characters
'القاهرة', // Arabic characters
'मुंबई', // Hindi characters
];
testCases.forEach((input) => {
expect(getNormalizedCity(input)).toBeNull();
});
});
it('should handle very long city names', () => {
const longCity = 'A'.repeat(100) + 'B'.repeat(50);
const result = getNormalizedCity(longCity);
expect(result).toEqual('a'.repeat(100) + 'b'.repeat(50)); // Lowercased
});
it('should return null for cities with only punctuation', () => {
const testCases = [
'...', // All punctuation stripped to empty
'!!!', // All punctuation stripped to empty
'---', // All punctuation stripped to empty
'(),[]{}', // All punctuation stripped to empty
'@#$%^&*()', // All punctuation stripped to empty
];
testCases.forEach((input) => {
expect(getNormalizedCity(input)).toBeNull();
});
});
it('should handle mixed Latin and non-Latin characters', () => {
const testCases = [
{
input: 'New York 北京',
expected: 'newyork', // Non-Latin characters stripped, only Latin remains
},
{
input: 'San Francisco サンフランシスコ',
expected: 'sanfrancisco', // Non-Latin characters stripped
},
{
input: 'London Londres',
expected: 'londonlondres', // All Latin characters preserved
},
{
input: 'Berlin Берлин',
expected: 'berlin', // Non-Latin characters stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
});
describe('Hashed city 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 = getNormalizedCity(hash);
expect(result).toEqual(hash);
});
});
it('should process invalid hash-like strings as regular city 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, starts with number
expected: null, // Fails regex test '^[a-z]+'
},
];
invalidHashes.forEach(({ input, expected }) => {
const result = getNormalizedCity(input);
expect(result).toEqual(expected);
});
});
it('should handle mixed case hashed cities', () => {
const mixedCaseHash =
'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3';
const result = getNormalizedCity(mixedCaseHash);
expect(result).toEqual(mixedCaseHash);
});
it('should handle lowercase hashed cities', () => {
const lowercaseHash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
const result = getNormalizedCity(lowercaseHash);
expect(result).toEqual(lowercaseHash);
});
});
describe('Edge cases', () => {
it('should handle numeric strings starting with numbers', () => {
const testCases = [
'123', // Starts with number, fails regex test
'0', // Starts with number, fails regex test
'456City', // Starts with number, fails regex test
'789Town', // Starts with number, fails regex test
];
testCases.forEach((input) => {
expect(getNormalizedCity(input)).toBeNull();
});
});
it('should handle strings that become empty after stripping', () => {
const testCases = [
'中文', // Non-Latin characters stripped to empty
'русский', // Non-Latin characters stripped to empty
'العربية', // Non-Latin characters stripped to empty
'한국어', // Non-Latin characters stripped to empty
'日本語', // Non-Latin characters stripped to empty
];
testCases.forEach((input) => {
expect(getNormalizedCity(input)).toBeNull();
});
});
it('should handle extreme cases', () => {
const testCases = [
{
input: String(Number.MAX_SAFE_INTEGER),
expected: null, // Starts with number, fails regex test
},
{
input: 'A'.repeat(1000), // Very long string
expected: 'a'.repeat(1000),
},
{
input: '!@#$%^&*()_+-=[]{}|;:,.<>?',
expected: null, // All punctuation stripped to empty, fails regex test
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
it('should handle Unicode and emoji', () => {
const testCases = [
{
input: 'London 😀',
expected: 'london', // Emoji stripped (non-Latin alpha-numeric)
},
{
input: 'Paris ❤️',
expected: 'paris', // Emoji stripped
},
{
input: '🎉 Party City',
expected: 'partycity', // Emoji and space stripped, starts with 'p'
},
{
input: '😀City',
expected: 'city', // Emoji stripped, starts with 'c'
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
});
});
});
});
describe('getNormalizedPII with CITY', () => {
describe('Input validation', () => {
it('should return null for null input', () => {
expect(getNormalizedPII(null, PII_DATA_TYPE.CITY)).toBeNull();
});
it('should return null for undefined input', () => {
expect(getNormalizedPII(undefined, PII_DATA_TYPE.CITY)).toBeNull();
});
it('should return null for non-string input', () => {
const nonStringValues = [123, {}, [], true, false];
nonStringValues.forEach((value) => {
expect(getNormalizedPII(value, PII_DATA_TYPE.CITY)).toBeNull();
});
});
it('should return null for empty string input', () => {
expect(getNormalizedPII('', PII_DATA_TYPE.CITY)).toBeNull();
});
it('should return null for whitespace-only input', () => {
expect(getNormalizedPII(' ', PII_DATA_TYPE.CITY)).toBeNull();
expect(getNormalizedPII('\t\n\r', PII_DATA_TYPE.CITY)).toBeNull();
});
it('should return null for null data type', () => {
expect(getNormalizedPII('London', null)).toBeNull();
});
it('should return null for undefined data type', () => {
expect(getNormalizedPII('London', undefined)).toBeNull();
});
it('should return null for invalid data type', () => {
expect(getNormalizedPII('London', 'invalid_type')).toBeNull();
});
});
describe('City normalization', () => {
it('should normalize city names using getNormalizedCity', () => {
const testCases = [
{
input: 'London',
expected: 'london',
},
{
input: 'NEW YORK',
expected: 'newyork',
},
{
input: ' San Francisco ',
expected: 'sanfrancisco',
},
{
input: 'St. Louis',
expected: 'stlouis',
},
{
input: 'Las Vegas',
expected: 'lasvegas',
},
{
input: 'Miami-Dade',
expected: 'miamidade',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).toEqual(expected);
});
});
it('should return null for cities starting with numbers', () => {
const testCases = ['1stCity', '2ndDistrict', '42ndStreet'];
testCases.forEach((input) => {
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).toBeNull();
});
});
it('should handle cities with diacritics and non-Latin characters', () => {
const testCases = [
{
input: '北京',
expected: null, // Pure Chinese, no Latin characters
},
{
input: 'Москва',
expected: null, // Pure Russian, no Latin characters
},
{
input: 'São Paulo',
expected: 'sopaulo', // Diacritics stripped, Latin characters preserved
},
{
input: 'México',
expected: 'mxico', // é character completely stripped
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).toEqual(expected);
});
});
it('should handle hashed city names', () => {
const hash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
expect(getNormalizedPII(hash, PII_DATA_TYPE.CITY)).toEqual(hash);
});
});
describe('Other PII data types', () => {
it('should return null for invalid email when using EMAIL data type', () => {
expect(getNormalizedPII('London', PII_DATA_TYPE.EMAIL)).toBeNull();
expect(getNormalizedPII('New York', PII_DATA_TYPE.EMAIL)).toBeNull();
});
it('should return normalized phone for city when using PHONE data type', () => {
expect(getNormalizedPII('London', PII_DATA_TYPE.PHONE)).toEqual('');
expect(getNormalizedPII('City123', PII_DATA_TYPE.PHONE)).toEqual('123');
expect(getNormalizedPII('123-456-7890', PII_DATA_TYPE.PHONE)).toEqual(
'1234567890'
);
});
it('should return normalized DOB for city when using DATE_OF_BIRTH data type', () => {
expect(
getNormalizedPII('London', PII_DATA_TYPE.DATE_OF_BIRTH)
).toBeNull(); // Invalid date
expect(
getNormalizedPII('New York', 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 city 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('London', PII_DATA_TYPE.GENDER)).toBeNull(); // Invalid gender term
});
it('should return normalized name for city when using NAME data types', () => {
expect(getNormalizedPII('London', PII_DATA_TYPE.FIRST_NAME)).toEqual(
'london'
);
expect(getNormalizedPII('New York', PII_DATA_TYPE.LAST_NAME)).toEqual(
'newyork'
);
expect(getNormalizedPII("St. John's", PII_DATA_TYPE.FIRST_NAME)).toEqual(
'stjohns'
);
});
it('should return null for truly unsupported data types', () => {
const unsupportedTypes = ['invalid_type', 'unknown', 'fake_type'];
unsupportedTypes.forEach((type) => {
expect(getNormalizedPII('London', type)).toBeNull();
});
});
});
describe('Comparison with direct getNormalizedCity', () => {
it('should show both functions behave the same way for valid cities', () => {
const testCases = [
{
input: 'London',
expected: 'london',
},
{
input: ' New York ',
expected: 'newyork',
},
{
input: 'St. Louis',
expected: 'stlouis',
},
{
input: 'Las Vegas',
expected: 'lasvegas',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).toEqual(expected);
});
});
it('should show both handle invalid cities the same way', () => {
const testCases = [
{
input: '1stCity',
expected: null, // Starts with number, fails regex test
},
{
input: '北京',
expected: null, // Non-Latin characters stripped to empty
},
{
input: '...',
expected: null, // All punctuation stripped to empty
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedCity(input)).toEqual(expected);
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).toEqual(expected);
});
});
it('should show both handle hashes the same way', () => {
const hash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
expect(getNormalizedCity(hash)).toEqual(hash);
expect(getNormalizedPII(hash, PII_DATA_TYPE.CITY)).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(getNormalizedCity(input)).toBeNull();
// PII interface also returns null for invalid inputs (validates input first)
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).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 = getNormalizedCity(input);
expect(
[null, 'string'].includes(typeof directResult) ||
directResult === null
).toBe(true);
// PII interface returns null for non-string inputs (validates input first)
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).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, then fails regex test)
const directResult = getNormalizedCity(input);
expect(directResult).toBeNull();
// PII interface also returns null (validates input first, but gets same result)
expect(getNormalizedPII(input, PII_DATA_TYPE.CITY)).toBeNull();
});
// Handle empty string separately since PII interface validates it differently
expect(getNormalizedCity('')).toBeNull();
expect(getNormalizedPII('', PII_DATA_TYPE.CITY)).toBeNull();
});
});
});