From f8b505ce670004cf20a5b3c902490ea2c790f416 Mon Sep 17 00:00:00 2001 From: Micah Baker Date: Thu, 5 Nov 2026 17:53:36 +0000 Subject: [PATCH 1/2] feat(pnv): reject test tokens by default and add PhoneNumberVerificationTokenOptions - Add TEST_TOKEN_REJECTED ('test-token-rejected') to PhoneNumberVerificationErrorCode and reject tokens containing '"test_number": true' by default unless options?.acceptTestTokens === true. - Add and export PhoneNumberVerificationTokenOptions ({ acceptTestTokens?: boolean }) on PhoneNumberVerification.verifyToken(jwt, options?) with runtime validation. - Populate required testNumber: boolean property on PhoneNumberVerificationToken. - Update unit tests and API Extractor report. --- ...ase-admin.phone-number-verification.api.md | 9 ++- src/phone-number-verification/error.ts | 1 + src/phone-number-verification/index.ts | 3 +- .../phone-number-verification-api.ts | 22 +++++++ .../phone-number-verification.ts | 42 ++++++++++++-- .../token-verifier.ts | 19 ++++++- .../phone-number-verification/index.spec.ts | 9 ++- ...r-verification-api-client-internal.spec.ts | 13 +++++ .../phone-number-verification.spec.ts | 37 +++++++++++- .../token-verifier.spec.ts | 57 ++++++++++++++++++- 10 files changed, 198 insertions(+), 14 deletions(-) diff --git a/etc/firebase-admin.phone-number-verification.api.md b/etc/firebase-admin.phone-number-verification.api.md index 256778f3fb..2db533d83b 100644 --- a/etc/firebase-admin.phone-number-verification.api.md +++ b/etc/firebase-admin.phone-number-verification.api.md @@ -22,7 +22,7 @@ export function getPhoneNumberVerification(app?: App): PhoneNumberVerification; // @public export class PhoneNumberVerification { get app(): App; - verifyToken(jwt: string): Promise; + verifyToken(jwt: string, options?: PhoneNumberVerificationTokenOptions): Promise; } // @public @@ -30,6 +30,7 @@ export const PhoneNumberVerificationErrorCode: { readonly INVALID_ARGUMENT: "invalid-argument"; readonly INVALID_TOKEN: "invalid-token"; readonly EXPIRED_TOKEN: "expired-token"; + readonly TEST_TOKEN_REJECTED: "test-token-rejected"; }; // @public @@ -46,6 +47,12 @@ export interface PhoneNumberVerificationToken { nonce: string; phoneNumber: string; sub: string; + testNumber: boolean; +} + +// @public +export interface PhoneNumberVerificationTokenOptions { + acceptTestTokens?: boolean; } ``` diff --git a/src/phone-number-verification/error.ts b/src/phone-number-verification/error.ts index 168c7bd055..95962c1898 100644 --- a/src/phone-number-verification/error.ts +++ b/src/phone-number-verification/error.ts @@ -23,6 +23,7 @@ export const PhoneNumberVerificationErrorCode = { INVALID_ARGUMENT: 'invalid-argument', INVALID_TOKEN: 'invalid-token', EXPIRED_TOKEN: 'expired-token', + TEST_TOKEN_REJECTED: 'test-token-rejected', } as const; /** diff --git a/src/phone-number-verification/index.ts b/src/phone-number-verification/index.ts index f27db5700c..963021b44f 100644 --- a/src/phone-number-verification/index.ts +++ b/src/phone-number-verification/index.ts @@ -31,7 +31,8 @@ export { export { PhoneNumberVerificationToken, -} from './phone-number-verification-api' + PhoneNumberVerificationTokenOptions, +} from './phone-number-verification-api'; /** * Gets the {@link PhoneNumberVerification} service for the default app or a diff --git a/src/phone-number-verification/phone-number-verification-api.ts b/src/phone-number-verification/phone-number-verification-api.ts index a3527d78d6..1ebc5de3d5 100644 --- a/src/phone-number-verification/phone-number-verification-api.ts +++ b/src/phone-number-verification/phone-number-verification-api.ts @@ -73,12 +73,34 @@ export interface PhoneNumberVerificationToken { */ phoneNumber: string; + /** + * Indicates whether the token was issued while the app was in test mode. + * This value is not actually one of the JWT token claims. It is added as a + * convenience, and is set to `true` when the `"test_number"` claim is + * `true`, or `false` otherwise. + */ + testNumber: boolean; + /** * Other arbitrary claims included in the token. */ [key: string]: any; } +/** + * Interface representing options for the {@link PhoneNumberVerification.verifyToken} method. + */ +export interface PhoneNumberVerificationTokenOptions { + /** + * To accept test-mode tokens (tokens containing the claim `"test_number": true`), + * set this option to `true`. By default, {@link PhoneNumberVerification.verifyToken} + * rejects test-mode tokens. + * + * Enable this option only during development and testing. + */ + acceptTestTokens?: boolean; +} + export { PhoneNumberVerificationErrorCode, FirebasePhoneNumberVerificationError, diff --git a/src/phone-number-verification/phone-number-verification.ts b/src/phone-number-verification/phone-number-verification.ts index 06e1818a68..28c14a1443 100644 --- a/src/phone-number-verification/phone-number-verification.ts +++ b/src/phone-number-verification/phone-number-verification.ts @@ -16,7 +16,13 @@ */ import { App } from '../app'; -import { PhoneNumberVerificationToken } from './phone-number-verification-api'; +import * as validator from '../utils/validator'; +import { + FirebasePhoneNumberVerificationError, + PhoneNumberVerificationToken, + PhoneNumberVerificationTokenOptions, +} from './phone-number-verification-api'; +import { FPNV_ERROR_CODE_MAPPING } from './error'; import { PhoneNumberTokenVerifier } from './token-verifier'; import { JWKS_URL, FPNV_TOKEN_INFO } from './phone-number-verification-api-client-internal'; @@ -53,13 +59,41 @@ export class PhoneNumberVerification { } /** - * Verifies a Firebase Phone Number Verification token. + * Verifies a Firebase Phone Number Verification token. By default, tokens issued in + * test mode (containing `"test_number": true`) are rejected unless + * `options.acceptTestTokens` is set to `true`. * * @param jwt - A string containing the Firebase Phone Number Verification JWT. + * @param options - Optional {@link PhoneNumberVerificationTokenOptions} object when verifying a + * Phone Number Verification token. * @returns A promise that resolves with the decoded token. */ - public verifyToken(jwt: string): Promise { - return this.phoneNumberVerificationVerifier.verifyJWT(jwt); + public verifyToken( + jwt: string, + options?: PhoneNumberVerificationTokenOptions + ): Promise { + this.validatePhoneNumberVerificationTokenOptions(options); + return this.phoneNumberVerificationVerifier.verifyJWT(jwt, options); + } + + private validatePhoneNumberVerificationTokenOptions( + options?: PhoneNumberVerificationTokenOptions + ): void { + if (typeof options === 'undefined') { + return; + } + if (!validator.isNonNullObject(options)) { + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'PhoneNumberVerificationTokenOptions must be a non-null object.', + }); + } + if (typeof options.acceptTestTokens !== 'undefined' && !validator.isBoolean(options.acceptTestTokens)) { + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.', + }); + } } } diff --git a/src/phone-number-verification/token-verifier.ts b/src/phone-number-verification/token-verifier.ts index 3eda268599..b437bad5fb 100644 --- a/src/phone-number-verification/token-verifier.ts +++ b/src/phone-number-verification/token-verifier.ts @@ -16,7 +16,11 @@ */ import { App } from '../app'; -import { FirebasePhoneNumberVerificationError, PhoneNumberVerificationToken } from './phone-number-verification-api'; +import { + FirebasePhoneNumberVerificationError, + PhoneNumberVerificationToken, + PhoneNumberVerificationTokenOptions, +} from './phone-number-verification-api'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { @@ -81,7 +85,10 @@ export class PhoneNumberTokenVerifier { // Project ID is validated in the verification call. } - public async verifyJWT(jwtToken: string): Promise { + public async verifyJWT( + jwtToken: string, + options?: PhoneNumberVerificationTokenOptions + ): Promise { if (!validator.isString(jwtToken)) { throw new FirebasePhoneNumberVerificationError({ code: FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN, @@ -92,7 +99,15 @@ export class PhoneNumberTokenVerifier { const projectId = await this.ensureProjectId(); const decoded = await this.decodeAndVerify(jwtToken, projectId); const decodedIdToken = decoded.payload as PhoneNumberVerificationToken; + if (decodedIdToken.test_number === true && options?.acceptTestTokens !== true) { + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.TEST_TOKEN_REJECTED, + message: `${this.tokenInfo.jwtName} is a test token. To accept test tokens during ` + + `development, pass { acceptTestTokens: true } to ${this.tokenInfo.verifyApiName}.`, + }); + } decodedIdToken.phoneNumber = decodedIdToken.sub; + decodedIdToken.testNumber = decodedIdToken.test_number === true; return decodedIdToken; } diff --git a/test/unit/phone-number-verification/index.spec.ts b/test/unit/phone-number-verification/index.spec.ts index 2911810987..9ed6d87d0d 100644 --- a/test/unit/phone-number-verification/index.spec.ts +++ b/test/unit/phone-number-verification/index.spec.ts @@ -34,9 +34,16 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('Module export', () => { - it('should export PhoneNumberVerification class', () => { + it('should export PhoneNumberVerification class and error types', () => { expect(pnvIndex.PhoneNumberVerification).to.exist; expect(pnvIndex.getPhoneNumberVerification).to.be.a('function'); + expect(pnvIndex.FirebasePhoneNumberVerificationError).to.be.a('function'); + expect(pnvIndex.PhoneNumberVerificationErrorCode).to.deep.equal({ + INVALID_ARGUMENT: 'invalid-argument', + INVALID_TOKEN: 'invalid-token', + EXPIRED_TOKEN: 'expired-token', + TEST_TOKEN_REJECTED: 'test-token-rejected', + }); }); }); diff --git a/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts b/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts index dd9b88f8c0..2413c9ac12 100644 --- a/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts +++ b/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts @@ -50,6 +50,7 @@ describe('FPNV Constants and Error Class', () => { INVALID_ARGUMENT: 'invalid-argument', INVALID_TOKEN: 'invalid-token', EXPIRED_TOKEN: 'expired-token', + TEST_TOKEN_REJECTED: 'test-token-rejected', }); }); }); @@ -74,6 +75,18 @@ describe('FPNV Constants and Error Class', () => { expect(error.message).to.equal(testMessage); }); + it('should match error codes using hasCode()', () => { + const error = new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.TEST_TOKEN_REJECTED, + message: 'Test token rejected.', + }); + + expect(error.code).to.equal(`${FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.TEST_TOKEN_REJECTED}`); + expect(error.hasCode(FPNV_ERROR_CODE_MAPPING.TEST_TOKEN_REJECTED)).to.be.true; + expect(error.hasCode(`${FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.TEST_TOKEN_REJECTED}`)).to.be.true; + expect(error.hasCode(FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN)).to.be.false; + }); + it('should handle all defined error codes', () => { const codes = Object.values(FPNV_ERROR_CODE_MAPPING); diff --git a/test/unit/phone-number-verification/phone-number-verification.spec.ts b/test/unit/phone-number-verification/phone-number-verification.spec.ts index e21ac19627..7901b5eb6a 100644 --- a/test/unit/phone-number-verification/phone-number-verification.spec.ts +++ b/test/unit/phone-number-verification/phone-number-verification.spec.ts @@ -24,7 +24,10 @@ import { App } from '../../../src/app/index'; import * as mocks from '../../resources/mocks'; import { PhoneNumberVerification } from '../../../src/phone-number-verification/phone-number-verification'; import { PhoneNumberTokenVerifier } from '../../../src/phone-number-verification/token-verifier'; -import { PhoneNumberVerificationToken } from '../../../src/phone-number-verification/phone-number-verification-api'; +import { + PhoneNumberVerificationToken, + PhoneNumberVerificationTokenOptions, +} from '../../../src/phone-number-verification/phone-number-verification-api'; describe('PhoneNumberVerification Service', () => { let phoneNumberVerificationService: PhoneNumberVerification; @@ -66,6 +69,7 @@ describe('PhoneNumberVerification Service', () => { jti: 'unique-token-id-123', nonce: 'random-nonce-string', phoneNumber: '+15555550100', + testNumber: false, }; it('should call the internal verifier with the provided JWT string', async () => { @@ -74,7 +78,36 @@ describe('PhoneNumberVerification Service', () => { await phoneNumberVerificationService.verifyToken(mockTokenString); expect(verifyJwtStub.calledOnce).to.be.true; - expect(verifyJwtStub.calledWith(mockTokenString)).to.be.true; + expect(verifyJwtStub.calledWith(mockTokenString, undefined)).to.be.true; + }); + + it('should pass PhoneNumberVerificationTokenOptions to the internal verifier', async () => { + verifyJwtStub.resolves(mockDecodedToken); + const options: PhoneNumberVerificationTokenOptions = { acceptTestTokens: true }; + + await phoneNumberVerificationService.verifyToken(mockTokenString, options); + + expect(verifyJwtStub.calledOnce).to.be.true; + expect(verifyJwtStub.calledWith(mockTokenString, options)).to.be.true; + }); + + it('should throw given invalid PhoneNumberVerificationTokenOptions', () => { + [null, 100, -100, 'abc', [], true].forEach((invalidOptions) => { + expect(() => { + return phoneNumberVerificationService.verifyToken(mockTokenString, invalidOptions as any); + }).to.throw('PhoneNumberVerificationTokenOptions must be a non-null object.'); + }); + }); + + it('should throw given non-boolean acceptTestTokens option', () => { + ['false', 'true', 0, 1, null, {}, []].forEach((invalidAcceptTestTokens) => { + expect(() => { + return phoneNumberVerificationService.verifyToken( + mockTokenString, + { acceptTestTokens: invalidAcceptTestTokens as any } + ); + }).to.throw('PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.'); + }); }); it('should return the decoded token object on success', async () => { diff --git a/test/unit/phone-number-verification/token-verifier.spec.ts b/test/unit/phone-number-verification/token-verifier.spec.ts index 9fe68fe42b..54c2719861 100644 --- a/test/unit/phone-number-verification/token-verifier.spec.ts +++ b/test/unit/phone-number-verification/token-verifier.spec.ts @@ -28,7 +28,11 @@ import { PhoneNumberTokenVerifier } from '../../../src/phone-number-verification import { FirebasePhoneNumberTokenInfo, } from '../../../src/phone-number-verification/phone-number-verification-api-client-internal'; -import { FPNV_ERROR_CODE_MAPPING } from '../../../src/phone-number-verification/error'; +import { + FirebasePhoneNumberVerificationError, + FPNV_ERROR_CODE_MAPPING, + PhoneNumberVerificationErrorCode, +} from '../../../src/phone-number-verification/error'; import * as mocks from '../../resources/mocks'; chai.use(chaiAsPromised); @@ -306,7 +310,7 @@ describe('PhoneNumberTokenVerifier', () => { }); describe('Success', () => { - it('should return the token with phoneNumber property appended', async () => { + it('should return the token with phoneNumber and testNumber properties appended', async () => { decodeJwtStub.resolves({ header: VALID_HEADER, payload: VALID_PAYLOAD }); signatureVerifierStub.verify.resolves(); @@ -316,9 +320,56 @@ describe('PhoneNumberTokenVerifier', () => { expect(result.sub).to.equal(VALID_PAYLOAD.sub); expect(result.aud).to.deep.equal(VALID_PAYLOAD.aud); - // Check the property + // Check the properties expect(result).to.have.property('phoneNumber'); expect(result.phoneNumber).to.equal(VALID_PAYLOAD.sub); + expect(result.testNumber).to.be.false; + }); + + it('should set testNumber to false when test_number claim is false or non-boolean', async () => { + decodeJwtStub.resolves({ + header: VALID_HEADER, + payload: { ...VALID_PAYLOAD, test_number: 'false' }, + }); + signatureVerifierStub.verify.resolves(); + + const result = await verifier.verifyJWT('valid-token'); + + expect(result.testNumber).to.be.false; + }); + }); + + describe('Test Token Handling', () => { + beforeEach(() => { + decodeJwtStub.resolves({ + header: VALID_HEADER, + payload: { ...VALID_PAYLOAD, test_number: true }, + }); + signatureVerifierStub.verify.resolves(); + }); + + it('should reject a test token by default with TEST_TOKEN_REJECTED', async () => { + const expectedCode = `${MOCK_FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.TEST_TOKEN_REJECTED}`; + const defaultRejectedError = await expect(verifier.verifyJWT('test-token')) + .to.be.rejectedWith(FirebasePhoneNumberVerificationError, /is a test token/); + expect(defaultRejectedError.code).to.equal(expectedCode); + expect(defaultRejectedError.hasCode(PhoneNumberVerificationErrorCode.TEST_TOKEN_REJECTED)).to.be.true; + + await expect(verifier.verifyJWT('test-token', { acceptTestTokens: false })) + .to.be.rejectedWith(FirebasePhoneNumberVerificationError, /is a test token/) + .and.eventually.have.property('code', expectedCode); + + await expect(verifier.verifyJWT('test-token', { acceptTestTokens: 'false' as any })) + .to.be.rejectedWith(FirebasePhoneNumberVerificationError, /is a test token/) + .and.eventually.have.property('code', expectedCode); + }); + + it('should accept a test token when acceptTestTokens is true and populate testNumber', async () => { + const result = await verifier.verifyJWT('test-token', { acceptTestTokens: true }); + + expect(result.test_number).to.be.true; + expect(result.testNumber).to.be.true; + expect(result.phoneNumber).to.equal(VALID_PAYLOAD.sub); }); }); From 1e01d83f923dce5430560df686bc51f11eeb5216 Mon Sep 17 00:00:00 2001 From: Micah Baker Date: Fri, 6 Nov 2026 20:52:06 +0000 Subject: [PATCH 2/2] fix(pnv): move options validation into verifyJWT and match interface indentation - Move validatePhoneNumberVerificationTokenOptions into PhoneNumberTokenVerifier.verifyJWT() after JWT string validation so invalid options reject on the promise path. - Assert FirebasePhoneNumberVerificationError and phone-number-verification/invalid-argument in unit tests. - Match 4-space indentation in PhoneNumberVerificationTokenOptions. --- .../phone-number-verification-api.ts | 16 +++--- .../phone-number-verification.ts | 24 -------- .../token-verifier.ts | 21 +++++++ .../phone-number-verification.spec.ts | 57 ++++++++++++++----- .../token-verifier.spec.ts | 48 +++++++++++++++- 5 files changed, 118 insertions(+), 48 deletions(-) diff --git a/src/phone-number-verification/phone-number-verification-api.ts b/src/phone-number-verification/phone-number-verification-api.ts index 1ebc5de3d5..cb18284cd9 100644 --- a/src/phone-number-verification/phone-number-verification-api.ts +++ b/src/phone-number-verification/phone-number-verification-api.ts @@ -91,14 +91,14 @@ export interface PhoneNumberVerificationToken { * Interface representing options for the {@link PhoneNumberVerification.verifyToken} method. */ export interface PhoneNumberVerificationTokenOptions { - /** - * To accept test-mode tokens (tokens containing the claim `"test_number": true`), - * set this option to `true`. By default, {@link PhoneNumberVerification.verifyToken} - * rejects test-mode tokens. - * - * Enable this option only during development and testing. - */ - acceptTestTokens?: boolean; + /** + * To accept test-mode tokens (tokens containing the claim `"test_number": true`), + * set this option to `true`. By default, {@link PhoneNumberVerification.verifyToken} + * rejects test-mode tokens. + * + * Enable this option only during development and testing. + */ + acceptTestTokens?: boolean; } export { diff --git a/src/phone-number-verification/phone-number-verification.ts b/src/phone-number-verification/phone-number-verification.ts index 28c14a1443..893c4d63eb 100644 --- a/src/phone-number-verification/phone-number-verification.ts +++ b/src/phone-number-verification/phone-number-verification.ts @@ -16,13 +16,10 @@ */ import { App } from '../app'; -import * as validator from '../utils/validator'; import { - FirebasePhoneNumberVerificationError, PhoneNumberVerificationToken, PhoneNumberVerificationTokenOptions, } from './phone-number-verification-api'; -import { FPNV_ERROR_CODE_MAPPING } from './error'; import { PhoneNumberTokenVerifier } from './token-verifier'; import { JWKS_URL, FPNV_TOKEN_INFO } from './phone-number-verification-api-client-internal'; @@ -72,28 +69,7 @@ export class PhoneNumberVerification { jwt: string, options?: PhoneNumberVerificationTokenOptions ): Promise { - this.validatePhoneNumberVerificationTokenOptions(options); return this.phoneNumberVerificationVerifier.verifyJWT(jwt, options); } - - private validatePhoneNumberVerificationTokenOptions( - options?: PhoneNumberVerificationTokenOptions - ): void { - if (typeof options === 'undefined') { - return; - } - if (!validator.isNonNullObject(options)) { - throw new FirebasePhoneNumberVerificationError({ - code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - message: 'PhoneNumberVerificationTokenOptions must be a non-null object.', - }); - } - if (typeof options.acceptTestTokens !== 'undefined' && !validator.isBoolean(options.acceptTestTokens)) { - throw new FirebasePhoneNumberVerificationError({ - code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - message: 'PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.', - }); - } - } } diff --git a/src/phone-number-verification/token-verifier.ts b/src/phone-number-verification/token-verifier.ts index b437bad5fb..771f1330e4 100644 --- a/src/phone-number-verification/token-verifier.ts +++ b/src/phone-number-verification/token-verifier.ts @@ -95,6 +95,7 @@ export class PhoneNumberTokenVerifier { message: `First argument to ${this.tokenInfo.verifyApiName} must be a string.`, }); } + this.validatePhoneNumberVerificationTokenOptions(options); const projectId = await this.ensureProjectId(); const decoded = await this.decodeAndVerify(jwtToken, projectId); @@ -111,6 +112,26 @@ export class PhoneNumberTokenVerifier { return decodedIdToken; } + private validatePhoneNumberVerificationTokenOptions( + options?: PhoneNumberVerificationTokenOptions + ): void { + if (typeof options === 'undefined') { + return; + } + if (!validator.isNonNullObject(options)) { + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'PhoneNumberVerificationTokenOptions must be a non-null object.', + }); + } + if (typeof options.acceptTestTokens !== 'undefined' && !validator.isBoolean(options.acceptTestTokens)) { + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.', + }); + } + } + private async ensureProjectId(): Promise { const projectId = await util.findProjectId(this.app); diff --git a/test/unit/phone-number-verification/phone-number-verification.spec.ts b/test/unit/phone-number-verification/phone-number-verification.spec.ts index 7901b5eb6a..33336f503b 100644 --- a/test/unit/phone-number-verification/phone-number-verification.spec.ts +++ b/test/unit/phone-number-verification/phone-number-verification.spec.ts @@ -17,7 +17,8 @@ 'use strict'; -import { expect } from 'chai'; +import * as chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import { App } from '../../../src/app/index'; @@ -25,10 +26,14 @@ import * as mocks from '../../resources/mocks'; import { PhoneNumberVerification } from '../../../src/phone-number-verification/phone-number-verification'; import { PhoneNumberTokenVerifier } from '../../../src/phone-number-verification/token-verifier'; import { + FirebasePhoneNumberVerificationError, PhoneNumberVerificationToken, PhoneNumberVerificationTokenOptions, } from '../../../src/phone-number-verification/phone-number-verification-api'; +chai.use(chaiAsPromised); +const expect = chai.expect; + describe('PhoneNumberVerification Service', () => { let phoneNumberVerificationService: PhoneNumberVerification; let mockApp: App; @@ -91,23 +96,47 @@ describe('PhoneNumberVerification Service', () => { expect(verifyJwtStub.calledWith(mockTokenString, options)).to.be.true; }); - it('should throw given invalid PhoneNumberVerificationTokenOptions', () => { - [null, 100, -100, 'abc', [], true].forEach((invalidOptions) => { - expect(() => { - return phoneNumberVerificationService.verifyToken(mockTokenString, invalidOptions as any); - }).to.throw('PhoneNumberVerificationTokenOptions must be a non-null object.'); - }); + it('should reject given invalid PhoneNumberVerificationTokenOptions', async () => { + verifyJwtStub.restore(); + for (const invalidOptions of [null, 100, -100, 'abc', [], true]) { + await expect( + phoneNumberVerificationService.verifyToken(mockTokenString, invalidOptions as any) + ) + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'PhoneNumberVerificationTokenOptions must be a non-null object.' + ) + .and.eventually.have.property('code', 'phone-number-verification/invalid-argument'); + } }); - it('should throw given non-boolean acceptTestTokens option', () => { - ['false', 'true', 0, 1, null, {}, []].forEach((invalidAcceptTestTokens) => { - expect(() => { - return phoneNumberVerificationService.verifyToken( + it('should reject given non-boolean acceptTestTokens option', async () => { + verifyJwtStub.restore(); + for (const invalidAcceptTestTokens of ['false', 'true', 0, 1, null, {}, []]) { + await expect( + phoneNumberVerificationService.verifyToken( mockTokenString, { acceptTestTokens: invalidAcceptTestTokens as any } - ); - }).to.throw('PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.'); - }); + ) + ) + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.' + ) + .and.eventually.have.property('code', 'phone-number-verification/invalid-argument'); + } + }); + + it('should validate jwt before options and reject on the promise path', async () => { + verifyJwtStub.restore(); + await expect( + phoneNumberVerificationService.verifyToken(123 as any, null as any) + ) + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'First argument to verifyToken() must be a string.' + ) + .and.eventually.have.property('code', 'phone-number-verification/invalid-token'); }); it('should return the decoded token object on success', async () => { diff --git a/test/unit/phone-number-verification/token-verifier.spec.ts b/test/unit/phone-number-verification/token-verifier.spec.ts index 54c2719861..101be0ab29 100644 --- a/test/unit/phone-number-verification/token-verifier.spec.ts +++ b/test/unit/phone-number-verification/token-verifier.spec.ts @@ -164,6 +164,44 @@ describe('PhoneNumberTokenVerifier', () => { .to.be.rejectedWith('First argument to verifyToken() must be a string.'); }); + it('should validate jwtToken before options', async () => { + await expect(verifier.verifyJWT(123 as any, null as any)) + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'First argument to verifyToken() must be a string.' + ) + .and.eventually.have.property( + 'code', + `${MOCK_FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN}` + ); + }); + + it('should reject with INVALID_ARGUMENT given invalid PhoneNumberVerificationTokenOptions', async () => { + const expectedCode = `${MOCK_FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT}`; + for (const invalidOptions of [null, 100, -100, 'abc', [], true]) { + await expect(verifier.verifyJWT('token', invalidOptions as any)) + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'PhoneNumberVerificationTokenOptions must be a non-null object.' + ) + .and.eventually.have.property('code', expectedCode); + } + }); + + it('should reject with INVALID_ARGUMENT given non-boolean acceptTestTokens option', async () => { + const expectedCode = `${MOCK_FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT}`; + for (const invalidAcceptTestTokens of ['false', 'true', 0, 1, null, {}, []]) { + await expect( + verifier.verifyJWT('token', { acceptTestTokens: invalidAcceptTestTokens as any }) + ) + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.' + ) + .and.eventually.have.property('code', expectedCode); + } + }); + it('should throw if project ID cannot be determined', async () => { findProjectIdStub.resolves(null); await expect(verifier.verifyJWT('token')) @@ -360,8 +398,14 @@ describe('PhoneNumberTokenVerifier', () => { .and.eventually.have.property('code', expectedCode); await expect(verifier.verifyJWT('test-token', { acceptTestTokens: 'false' as any })) - .to.be.rejectedWith(FirebasePhoneNumberVerificationError, /is a test token/) - .and.eventually.have.property('code', expectedCode); + .to.be.rejectedWith( + FirebasePhoneNumberVerificationError, + 'PhoneNumberVerificationTokenOptions.acceptTestTokens must be a boolean.' + ) + .and.eventually.have.property( + 'code', + `${MOCK_FPNV_PREFIX}/${FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT}` + ); }); it('should accept a test token when acceptTestTokens is true and populate testNumber', async () => {