Skip to content

Commit b8606ef

Browse files
committed
test(analytics): cover hashed conversion SHA-256 hex validation
1 parent 1bb1145 commit b8606ef

3 files changed

Lines changed: 227 additions & 5 deletions

File tree

packages/analytics/__tests__/analytics.test.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,98 @@ describe('Analytics', function () {
270270
);
271271
});
272272

273+
it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` throws if not a string before E.164', function () {
274+
expect(() =>
275+
// @ts-ignore
276+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), true),
277+
).toThrow(
278+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
279+
);
280+
});
281+
273282
it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` should throw if the value is in E.164 format', function () {
274283
expect(() =>
275284
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), '+1234567890'),
276285
).toThrow(
277-
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.",
286+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a 64-character SHA-256 hex string of a phone number in E.164 format, not an E.164 number.",
278287
);
279288
});
280289

290+
describe('SHA-256 hex validation for hashed on-device conversion', function () {
291+
const validLower = '0123456789abcdef'.repeat(4);
292+
const validUpper = '0123456789ABCDEF'.repeat(4);
293+
const empty = '';
294+
const shortEven = validLower.slice(0, 32);
295+
const oddLength = validLower.slice(0, 63);
296+
const tooLong = `${validLower}aa`;
297+
const nonHex = 'g'.repeat(64);
298+
299+
const hashedEmailHexError =
300+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a 64-character SHA-256 hex string.";
301+
const hashedPhoneHexError =
302+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a 64-character SHA-256 hex string.";
303+
304+
it.each([
305+
['empty', empty],
306+
['short', shortEven],
307+
['odd-length', oddLength],
308+
['too long', tooLong],
309+
['non-hex', nonHex],
310+
])(
311+
'`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` rejects a %s value',
312+
function (_label, hashedEmailAddress) {
313+
expect(() =>
314+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
315+
getAnalytics(),
316+
hashedEmailAddress,
317+
),
318+
).toThrow(hashedEmailHexError);
319+
},
320+
);
321+
322+
it.each([
323+
['empty', empty],
324+
['short', shortEven],
325+
['odd-length', oddLength],
326+
['too long', tooLong],
327+
['non-hex', nonHex],
328+
])(
329+
'`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` rejects a %s value',
330+
function (_label, hashedPhoneNumber) {
331+
expect(() =>
332+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
333+
getAnalytics(),
334+
hashedPhoneNumber,
335+
),
336+
).toThrow(hashedPhoneHexError);
337+
},
338+
);
339+
340+
it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` accepts a lowercase 64-character hex string', async function () {
341+
await expect(
342+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress(getAnalytics(), validLower),
343+
).resolves.toBeUndefined();
344+
});
345+
346+
it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` accepts an uppercase 64-character hex string', async function () {
347+
await expect(
348+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress(getAnalytics(), validUpper),
349+
).resolves.toBeUndefined();
350+
});
351+
352+
it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` accepts a lowercase 64-character hex string', async function () {
353+
await expect(
354+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), validLower),
355+
).resolves.toBeUndefined();
356+
});
357+
358+
it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` accepts an uppercase 64-character hex string', async function () {
359+
await expect(
360+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), validUpper),
361+
).resolves.toBeUndefined();
362+
});
363+
});
364+
281365
it('`initiateOnDeviceConversionMeasurementWithPhoneNumber` function is properly exposed to end user', function () {
282366
expect(initiateOnDeviceConversionMeasurementWithPhoneNumber).toBeDefined();
283367
});

packages/analytics/e2e/analytics.e2e.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,144 @@ describe('analytics()', function () {
593593
});
594594
});
595595

596+
describe('on-device conversion measurement with hashed credentials', function () {
597+
const validLower = '0123456789abcdef'.repeat(4);
598+
const validUpper = '0123456789ABCDEF'.repeat(4);
599+
const empty = '';
600+
const shortEven = validLower.slice(0, 32);
601+
const oddLength = validLower.slice(0, 63);
602+
const tooLong = `${validLower}aa`;
603+
const nonHex = 'g'.repeat(64);
604+
605+
function expectHexContractError(error, apiLabel) {
606+
const message = error + '';
607+
if (!message.includes('64-character SHA-256 hex string')) {
608+
fail(`Should have returned a hex-length error for ${apiLabel}: ${message}`);
609+
}
610+
}
611+
612+
[
613+
['empty', empty],
614+
['short', shortEven],
615+
['odd-length', oddLength],
616+
['too long', tooLong],
617+
['non-hex', nonHex],
618+
].forEach(function ([label, value]) {
619+
it(`rejects a ${label} hashed email via JS validation`, async function () {
620+
try {
621+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedEmailAddress } =
622+
analyticsModular;
623+
await initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
624+
getAnalytics(),
625+
value,
626+
);
627+
fail(`Should have returned an error for ${label} hashed email`);
628+
} catch (e) {
629+
expectHexContractError(e, `hashed email (${label})`);
630+
}
631+
});
632+
633+
it(`rejects a ${label} hashed phone via JS validation`, async function () {
634+
try {
635+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedPhoneNumber } =
636+
analyticsModular;
637+
await initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), value);
638+
fail(`Should have returned an error for ${label} hashed phone`);
639+
} catch (e) {
640+
expectHexContractError(e, `hashed phone (${label})`);
641+
}
642+
});
643+
});
644+
645+
it('rejects an E.164 phone number used as a hashed phone', async function () {
646+
try {
647+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedPhoneNumber } =
648+
analyticsModular;
649+
await initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
650+
getAnalytics(),
651+
'+14155551212',
652+
);
653+
fail('Should have returned an error for an E.164 hashed phone');
654+
} catch (e) {
655+
const message = e + '';
656+
if (!message.includes('64-character SHA-256 hex string') || !message.includes('E.164')) {
657+
fail(`Should have returned an E.164 hashed-phone error: ${message}`);
658+
}
659+
}
660+
});
661+
662+
it('accepts a lowercase 64-character hashed email (reaches native on iOS)', async function () {
663+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedEmailAddress } =
664+
analyticsModular;
665+
await initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
666+
getAnalytics(),
667+
validLower,
668+
);
669+
});
670+
671+
it('accepts an uppercase 64-character hashed email (reaches native on iOS)', async function () {
672+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedEmailAddress } =
673+
analyticsModular;
674+
await initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
675+
getAnalytics(),
676+
validUpper,
677+
);
678+
});
679+
680+
it('accepts a lowercase 64-character hashed phone (reaches native on iOS)', async function () {
681+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedPhoneNumber } =
682+
analyticsModular;
683+
await initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
684+
getAnalytics(),
685+
validLower,
686+
);
687+
});
688+
689+
it('accepts an uppercase 64-character hashed phone (reaches native on iOS)', async function () {
690+
const { getAnalytics, initiateOnDeviceConversionMeasurementWithHashedPhoneNumber } =
691+
analyticsModular;
692+
await initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
693+
getAnalytics(),
694+
validUpper,
695+
);
696+
});
697+
698+
// Bypass JS validation so iOS decoder reject paths actually execute.
699+
[
700+
['empty', empty],
701+
['odd-length', oddLength],
702+
['non-hex', nonHex],
703+
].forEach(function ([label, value]) {
704+
it(`iOS native decoder rejects a ${label} hashed email`, async function () {
705+
if (!Platform.ios) {
706+
this.skip();
707+
}
708+
try {
709+
await NativeModules.NativeRNFBTurboAnalytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
710+
value,
711+
);
712+
fail(`Native hashed email should have rejected ${label}`);
713+
} catch (e) {
714+
expectHexContractError(e, `native hashed email (${label})`);
715+
}
716+
});
717+
718+
it(`iOS native decoder rejects a ${label} hashed phone`, async function () {
719+
if (!Platform.ios) {
720+
this.skip();
721+
}
722+
try {
723+
await NativeModules.NativeRNFBTurboAnalytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
724+
value,
725+
);
726+
fail(`Native hashed phone should have rejected ${label}`);
727+
} catch (e) {
728+
expectHexContractError(e, `native hashed phone (${label})`);
729+
}
730+
});
731+
});
732+
});
733+
596734
// Test this last so it does not stop delivery to DebugView
597735
describe('setAnalyticsCollectionEnabled()', function () {
598736
it('false', async function () {

packages/analytics/lib/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -920,15 +920,15 @@ class FirebaseAnalyticsModule extends FirebaseModule<typeof nativeModuleName> {
920920
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
921921
hashedPhoneNumber: string,
922922
): Promise<void> {
923-
if (isE164PhoneNumber(hashedPhoneNumber)) {
923+
if (!isString(hashedPhoneNumber)) {
924924
throw new Error(
925-
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.",
925+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
926926
);
927927
}
928928

929-
if (!isString(hashedPhoneNumber)) {
929+
if (isE164PhoneNumber(hashedPhoneNumber)) {
930930
throw new Error(
931-
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
931+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a 64-character SHA-256 hex string of a phone number in E.164 format, not an E.164 number.",
932932
);
933933
}
934934

0 commit comments

Comments
 (0)