meta-capi-param-builder-clientjs
Version:
Conversions API parameter builder for Client-side JavaScript
848 lines (753 loc) • 25.1 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 { getNormalizedDOB } from '../utils/piiUtil/dobUtil.js';
import { PII_DATA_TYPE } from '../model/constants.js';
describe('getNormalizedDOB', () => {
describe('Input validation', () => {
let consoleErrorSpy;
beforeEach(() => {
consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
it('should return null for null input', () => {
expect(getNormalizedDOB(null)).toBeNull();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to normalize dob: ',
expect.any(TypeError)
);
});
it('should return null for undefined input', () => {
expect(getNormalizedDOB(undefined)).toBeNull();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to normalize dob: ',
expect.any(TypeError)
);
});
it('should return null for non-string input', () => {
const nonStringValues = [123, {}, [], true, false];
nonStringValues.forEach((value) => {
expect(getNormalizedDOB(value)).toBeNull();
});
expect(consoleErrorSpy).toHaveBeenCalledTimes(nonStringValues.length);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to normalize dob: ',
expect.any(TypeError)
);
});
it('should return null for empty string input', () => {
expect(getNormalizedDOB('')).toBeNull();
});
it('should return null for whitespace-only input', () => {
expect(getNormalizedDOB(' ')).toBeNull();
expect(getNormalizedDOB('\t\n\r')).toBeNull();
});
});
describe('Date parsing from different formats', () => {
it('should parse MM-DD-YYYY format', () => {
const testCases = [
{
input: '12-31-1990',
expected: '19901231',
},
{
input: '01-15-2000',
expected: '20000115',
},
{
input: '06-30-1985',
expected: '19850630',
},
{
input: '03-05-1995',
expected: '19950305',
},
{
input: '02/16/1997 (mm/dd/yyyy)',
expected: '19970216',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should parse DD-MM-YYYY format', () => {
const testCases = [
{
input: '31-12-1990',
expected: '19901231',
},
{
input: '15-01-2000',
expected: '20000115',
},
{
input: '30-06-1985',
expected: '19850630',
},
{
input: '05-03-1995',
expected: '19950503', // Logic treats 05 as month, 03 as day when no number > 31
},
{
input: '16-2-1997 (dd-mm-yyyy)',
expected: '19970216',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should parse YYYY-MM-DD format', () => {
const testCases = [
{
input: '1990-12-31',
expected: '19901231',
},
{
input: '2000-01-15',
expected: '20000115',
},
{
input: '1985-06-30',
expected: '19850630',
},
{
input: '1995-03-05',
expected: '19950305',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should parse dates with various separators', () => {
const testCases = [
{
input: '12/31/1990',
expected: '19901231',
},
{
input: '1990.12.31',
expected: '19901231',
},
{
input: '12 31 1990',
expected: '19901231',
},
{
input: '1990_12_31',
expected: '19901231',
},
{
input: '12:31:1990',
expected: '19901231',
},
{
input: '1990|12|31',
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle dates with mixed separators', () => {
const testCases = [
{
input: '12-31/1990',
expected: '19901231',
},
{
input: '1990.12-31',
expected: '19901231',
},
{
input: '12 31/1990',
expected: '19901231',
},
{
input: '1990_12.31',
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle dates with leading/trailing whitespace', () => {
const testCases = [
{
input: ' 12-31-1990 ',
expected: '19901231',
},
{
input: '\t1990-12-31\n',
expected: '19901231',
},
{
input: ' 31/12/1990 ',
expected: '19901231',
},
{
input: '\r\n1990.12.31\t',
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle 8-digit date strings', () => {
const testCases = [
{
input: '19901231',
expected: '19901231',
},
{
input: '20000115',
expected: '20000115',
},
{
input: '19850630',
expected: '19850630',
},
{
input: '19950305',
expected: '19950305',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle dates with leading zeros', () => {
const testCases = [
{
input: '01-02-1990',
expected: '19900102',
},
{
input: '1990-01-02',
expected: '19900102',
},
{
input: '02-01-1990',
expected: '19900201',
},
{
input: '05-09-2000',
expected: '20000509',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle ambiguous dates consistently', () => {
// For ambiguous dates, the logic determines format based on which number > 31 and > 12
const testCases = [
{
input: '01-02-1990', // Month-Day-Year (since 1990 > 31, so 1990 is year, 01 is month, 02 is day)
expected: '19900102',
},
{
input: '13-02-1990', // Day-Month-Year (since 13 > 12, treat 13 as day, 02 as month, 1990 as year)
expected: '19900213',
},
{
input: '02-13-1990', // Month-Day-Year (since 13 > 12, treat 02 as month, 13 as day, 1990 as year)
expected: '19900213',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
});
describe('Date validation', () => {
it('should handle valid dates within acceptable year range', () => {
const currentYear = new Date().getFullYear();
const testCases = [
{
input: '01-01-1800',
expected: '18000101',
},
{
input: `01-01-${currentYear}`,
expected: `${currentYear}0101`,
},
{
input: `01-01-${currentYear + 1}`,
expected: `${currentYear + 1}0101`,
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should return null for invalid years', () => {
const currentYear = new Date().getFullYear();
const testCases = [
'01-01-1799', // Year too early
`01-01-${currentYear + 2}`, // Year too far in future
'01-01-0100', // Year too early
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
it('should return null for invalid months', () => {
const testCases = [
'00-15-1990', // Month 0
'13-15-1990', // Month 13 (both 13 and 15 > 12, invalid)
'15-13-1990', // Month 15 (both 15 and 13 > 12, invalid)
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
it('should return null for invalid days', () => {
const testCases = [
'01-00-1990', // Day 0
'01-32-1990', // Day 32
'32-01-1990', // Day 32 (invalid)
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
it('should handle leap year dates', () => {
const testCases = [
{
input: '02-29-2000', // Valid leap year
expected: '20000229',
},
{
input: '29-02-2000', // Valid leap year (DD-MM-YYYY)
expected: '20000229',
},
{
input: '2000-02-29', // Valid leap year (YYYY-MM-DD)
expected: '20000229',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
});
describe('Hashed DOB 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 = getNormalizedDOB(hash);
expect(result).toEqual(hash);
});
});
it('should return null for invalid hash-like strings that cannot be parsed as dates', () => {
const invalidHashes = [
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae', // 63 chars - no valid digits for parsing
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae33', // 65 chars - no valid digits for parsing
'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', // invalid char 'g' - no valid digits for parsing
'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE!', // invalid char '!' - no valid digits for parsing
'123', // too short for hash and insufficient digits for date parsing
];
invalidHashes.forEach((input) => {
const result = getNormalizedDOB(input);
expect(result).toBeNull();
});
});
it('should handle mixed case hashed DOB', () => {
const mixedCaseHash =
'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3';
const result = getNormalizedDOB(mixedCaseHash);
expect(result).toEqual(mixedCaseHash);
});
it('should handle lowercase hashed DOB', () => {
const lowercaseHash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
const result = getNormalizedDOB(lowercaseHash);
expect(result).toEqual(lowercaseHash);
});
});
describe('Edge cases', () => {
it('should return null for insufficient digit groups', () => {
const testCases = [
'12-31', // Only 2 groups, insufficient for date parsing
'1990', // Only 1 group, but not 8 digits
'12', // Only 1 group, too short
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
it('should handle dates with extra digit groups', () => {
const testCases = [
{
input: '12-31-1990-15-30', // 5 groups, should use first 3
expected: '19901231',
},
{
input: '1990-12-31-00-00-00', // 6 groups, should use first 3
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle non-digit characters mixed with dates', () => {
const testCases = [
{
input: 'Born: 12-31-1990',
expected: '19901231',
},
{
input: 'DOB is 1990/12/31 (verified)',
expected: '19901231',
},
{
input: 'Date: 31.12.1990 Age: 30',
expected: '19901231',
},
{
input: '12-31-1990 (mm-dd-yyyy)',
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should return null for dates with invalid years (too low)', () => {
const testCases = [
'01-01-01', // Year 1 is too low (< 1800)
'12-12-12', // Year 12 is too low (< 1800)
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
it('should handle Unicode and international characters', () => {
const testCases = [
{
input: '12-31-1990', // Full-width characters - no ASCII digits found
expected: null, // Returns null when no valid ASCII digits are found
},
{
input: '12月31日1990年', // Japanese date format - has ASCII digits
expected: '19901231', // Extract digits: 12, 31, 1990
},
{
input: '31/12/1990 (DD/MM/YYYY)',
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should return null for invalid date strings that cannot be parsed', () => {
// Test cases that cannot be parsed as dates - new implementation returns null
const testCases = [
'invalid date string', // No valid digit sequences for parsing
'---',
'...',
'abc-def-ghi',
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
it('should return null for very long digit sequences that are not exactly 8 digits', () => {
const testCases = [
'199012311999', // 12 digits, not exactly 8 - cannot be parsed as valid date
'1990123', // 7 digits, not exactly 8 - cannot be parsed as valid date
'123456789', // 9 digits, not exactly 8 - cannot be parsed as valid date
];
testCases.forEach((input) => {
expect(getNormalizedDOB(input)).toBeNull();
});
});
});
describe('Real-world date formats', () => {
it('should handle common US date formats', () => {
const testCases = [
{
input: 'December 31, 1990',
expected: null, // Only 2 digit groups: [31, 1990], insufficient for date parsing
},
{
input: 'Dec 31, 1990',
expected: null, // Only 2 digit groups: [31, 1990], insufficient for date parsing
},
{
input: '12/31/90', // 3 digit groups: [12, 31, 90] - year 90 is too low (< 1800)
expected: null, // Year validation fails
},
];
testCases.forEach(({ input, expected }) => {
const result = getNormalizedDOB(input);
expect(result).toEqual(expected);
});
});
it('should handle European date formats', () => {
const testCases = [
{
input: '31.12.1990',
expected: '19901231',
},
{
input: '31/12/1990',
expected: '19901231',
},
{
input: '31-12-1990',
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
it('should handle ISO date formats', () => {
const testCases = [
{
input: '1990-12-31',
expected: '19901231',
},
{
input: '1990/12/31',
expected: '19901231',
},
{
input: '19901231', // Already in YYYYMMDD format
expected: '19901231',
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
});
});
});
});
describe('getNormalizedPII with DATE_OF_BIRTH', () => {
describe('Input validation', () => {
it('should return null for null input', () => {
expect(getNormalizedPII(null, PII_DATA_TYPE.DATE_OF_BIRTH)).toBeNull();
});
it('should return null for undefined input', () => {
expect(
getNormalizedPII(undefined, PII_DATA_TYPE.DATE_OF_BIRTH)
).toBeNull();
});
it('should return null for non-string input', () => {
const nonStringValues = [123, {}, [], true, false];
nonStringValues.forEach((value) => {
expect(getNormalizedPII(value, PII_DATA_TYPE.DATE_OF_BIRTH)).toBeNull();
});
});
it('should return null for empty string input', () => {
expect(getNormalizedPII('', PII_DATA_TYPE.DATE_OF_BIRTH)).toBeNull();
});
it('should return null for null data type', () => {
expect(getNormalizedPII('12-31-1990', null)).toBeNull();
});
it('should return null for undefined data type', () => {
expect(getNormalizedPII('12-31-1990', undefined)).toBeNull();
});
it('should return null for invalid data type', () => {
expect(getNormalizedPII('12-31-1990', 'invalid_type')).toBeNull();
});
});
describe('DATE_OF_BIRTH data type handling', () => {
it('should return normalized value for DATE_OF_BIRTH data type', () => {
const testCases = [
{
input: '12-31-1990',
expected: '19901231',
},
{
input: '1990-12-31',
expected: '19901231',
},
{
input: '31/12/1990',
expected: '19901231',
},
{
input: ' 12-31-1990 ',
expected: '19901231',
},
{
input: '19901231',
expected: '19901231',
},
{
input: 'MIXED-case-date',
expected: null, // Invalid date string - no valid digits for parsing
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedPII(input, PII_DATA_TYPE.DATE_OF_BIRTH)).toEqual(
expected
);
});
});
it('should handle whitespace-only DOB', () => {
// The new getNormalizedDOB returns null for whitespace-only strings
// PII interface passes through this null result
expect(getNormalizedPII(' ', PII_DATA_TYPE.DATE_OF_BIRTH)).toBeNull();
expect(
getNormalizedPII('\t\n\r', PII_DATA_TYPE.DATE_OF_BIRTH)
).toBeNull();
});
});
describe('Other PII data types', () => {
it('should return normalized gender for DOB 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('12-31-1990', PII_DATA_TYPE.GENDER)).toBeNull(); // Invalid gender term
});
it('should return null for invalid email when using EMAIL data type', () => {
expect(getNormalizedPII('12-31-1990', PII_DATA_TYPE.EMAIL)).toBeNull();
expect(getNormalizedPII('1990-12-31', PII_DATA_TYPE.EMAIL)).toBeNull();
});
it('should return normalized phone for DOB when using PHONE data type', () => {
expect(getNormalizedPII('12-31-1990', PII_DATA_TYPE.PHONE)).toEqual(
'12311990'
);
expect(getNormalizedPII('1990-12-31', PII_DATA_TYPE.PHONE)).toEqual(
'19901231'
);
});
it('should return null for truly unsupported data types', () => {
const unsupportedTypes = ['invalid_type', 'unknown', 'fake_type'];
unsupportedTypes.forEach((type) => {
expect(getNormalizedPII('12-31-1990', type)).toBeNull();
});
});
});
describe('Comparison with direct getNormalizedDOB', () => {
it('should show both functions behave the same way for valid dates', () => {
const testCases = [
{
input: '12-31-1990',
expected: '19901231', // Both functions normalize
},
{
input: ' 1990/12/31 ',
expected: '19901231', // Both functions normalize
},
{
input: 'Born: 31.12.1990',
expected: '19901231', // Both functions extract and normalize
},
{
input: '19901231',
expected: '19901231', // Both functions keep 8-digit format
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
expect(getNormalizedPII(input, PII_DATA_TYPE.DATE_OF_BIRTH)).toEqual(
expected
);
});
});
it('should show both handle invalid dates the same way', () => {
const testCases = [
{
input: 'invalid-date-string',
expected: null, // Both functions return null on parse failure in new implementation
},
{
input: '32-13-1990', // Invalid date
expected: null, // Both functions return null for invalid dates in new implementation
},
{
input: '12-31', // Insufficient groups
expected: null, // Both functions return null for insufficient digit groups in new implementation
},
];
testCases.forEach(({ input, expected }) => {
expect(getNormalizedDOB(input)).toEqual(expected);
expect(getNormalizedPII(input, PII_DATA_TYPE.DATE_OF_BIRTH)).toEqual(
expected
);
});
});
it('should show both handle hashes the same way', () => {
const hash =
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
expect(getNormalizedDOB(hash)).toEqual(hash);
expect(getNormalizedPII(hash, PII_DATA_TYPE.DATE_OF_BIRTH)).toEqual(hash);
});
it('should show different handling for null/invalid inputs due to PII interface validation', () => {
const invalidInputs = [null, undefined, 123, {}, []];
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
invalidInputs.forEach((input) => {
// Direct function now returns null for all non-string inputs due to try-catch error handling
const directResult = getNormalizedDOB(input);
expect(directResult).toBeNull();
// PII interface also returns null for invalid inputs (validates input first)
expect(getNormalizedPII(input, PII_DATA_TYPE.DATE_OF_BIRTH)).toBeNull();
});
// Verify that console.error was called for the direct getNormalizedDOB calls
expect(consoleErrorSpy).toHaveBeenCalledTimes(invalidInputs.length);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to normalize dob: ',
expect.any(TypeError)
);
consoleErrorSpy.mockRestore();
});
it('should show both handle whitespace and edge cases the same way', () => {
const testCases = [
{
input: ' ',
expected: null, // Both return null for whitespace-only in new implementation
},
{
input: '\t\n\r',
expected: null, // Both return null for whitespace-only in new implementation
},
{
input: '',
expected: null, // Both return null for empty string in new implementation
},
];
testCases.forEach(({ input, expected }) => {
if (input === '') {
// Empty string case: both functions return null in new implementation
expect(getNormalizedDOB(input)).toBeNull();
expect(
getNormalizedPII(input, PII_DATA_TYPE.DATE_OF_BIRTH)
).toBeNull();
} else {
expect(getNormalizedDOB(input)).toEqual(expected);
expect(getNormalizedPII(input, PII_DATA_TYPE.DATE_OF_BIRTH)).toEqual(
expected
);
}
});
});
});
});