diff --git a/src/authToken.spec.ts b/src/authToken.spec.ts index 0396a081..a3adc792 100644 --- a/src/authToken.spec.ts +++ b/src/authToken.spec.ts @@ -1,6 +1,9 @@ -import { getAuthenticationToken, resetCachedAuthToken } from './authToken'; +import { getAuthenticationToken, resetCachedAuthToken, validateAuthToken } from './authToken'; import * as authServiceInstance from './utils/authService/authService'; import { EmbedConfig } from './types'; +import { formatTemplate } from './utils'; +import { logger } from './utils/logger'; +import { ERROR_MESSAGE } from './errors'; describe('AuthToken Unit tests', () => { test('getAuthenticationToken: When verification is disabled', async () => { @@ -28,4 +31,48 @@ describe('AuthToken Unit tests', () => { expect(token).toBe('abc2'); expect(authServiceInstance.verifyTokenService).toBeCalledWith('test', 'abc2'); }); + + test('validateAuthToken : When token is invalid by type number', async () => { + jest.spyOn(logger, 'error').mockImplementation(() => {}); + const loggerSpy = jest.spyOn(logger, 'error'); + + const authToken = (123 as unknown) as string; + const errorMessage = formatTemplate(ERROR_MESSAGE.INVALID_TOKEN_TYPE_ERROR, { + invalidType: typeof authToken, + }); + + await expect( + validateAuthToken( + { + thoughtSpotHost: 'test', + } as EmbedConfig, + authToken, + ), + ).rejects.toThrow(errorMessage); + expect(loggerSpy).toHaveBeenCalledWith(errorMessage); + + loggerSpy.mockRestore(); + }); + + test('validateAuthToken : When token is invalid by type object', async () => { + jest.spyOn(logger, 'error').mockImplementation(() => {}); + const loggerSpy = jest.spyOn(logger, 'error'); + + const authToken = ({} as unknown) as string; + const errorMessage = formatTemplate(ERROR_MESSAGE.INVALID_TOKEN_TYPE_ERROR, { + invalidType: typeof authToken, + }); + + await expect( + validateAuthToken( + { + thoughtSpotHost: 'test', + } as EmbedConfig, + authToken, + ), + ).rejects.toThrow(errorMessage); + expect(loggerSpy).toHaveBeenCalledWith(errorMessage); + + loggerSpy.mockRestore(); + }); }); diff --git a/src/authToken.ts b/src/authToken.ts index cbfdea8d..14cb0407 100644 --- a/src/authToken.ts +++ b/src/authToken.ts @@ -1,6 +1,6 @@ import { ERROR_MESSAGE } from './errors'; import { EmbedConfig } from './types'; -import { getValueFromWindow, storeValueInWindow } from './utils'; +import { getValueFromWindow, storeValueInWindow, formatTemplate } from './utils'; import { fetchAuthTokenService, verifyTokenService } from './utils/authService/authService'; import { logger } from './utils/logger'; @@ -53,12 +53,23 @@ export async function getAuthenticationToken(embedConfig: EmbedConfig): Promise< return authToken; } -const validateAuthToken = async ( +export const validateAuthToken = async ( embedConfig: EmbedConfig, authToken: string, suppressAlert?: boolean, ): Promise => { + // even if token verification is disabled, we will still validate + // that the token is a string before proceeding. + if (typeof authToken !== 'string') { + const errorMessage = formatTemplate(ERROR_MESSAGE.INVALID_TOKEN_TYPE_ERROR, { + invalidType: typeof authToken, + }); + logger.error(errorMessage); + throw new Error(errorMessage); + } + const cachedAuthToken = getCacheAuthToken(); + if (embedConfig.disableTokenVerification) { logger.info('Token verification is disabled. Assuming token is valid.'); return true; diff --git a/src/errors.ts b/src/errors.ts index efbfc3ed..7cee84e6 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -10,6 +10,7 @@ export const ERROR_MESSAGE = { SDK_NOT_INITIALIZED: 'SDK not initialized', SESSION_INFO_FAILED: 'Failed to get session information', INVALID_TOKEN_ERROR: 'Received invalid token from getAuthToken callback or authToken endpoint.', + INVALID_TOKEN_TYPE_ERROR: 'Expected getAuthToken to return a string, but received a {invalidType}.', MIXPANEL_TOKEN_NOT_FOUND: 'Mixpanel token not found in session info', PRERENDER_ID_MISSING: 'PreRender ID is required for preRender', SYNC_STYLE_CALLED_BEFORE_RENDER: 'PreRender should be called before using syncPreRenderStyle', diff --git a/src/utils.spec.ts b/src/utils.spec.ts index 06f91be5..e50cdf63 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -18,6 +18,7 @@ import { getTypeFromValue, arrayIncludesString, calculateVisibleElementData, + formatTemplate, } from './utils'; import { RuntimeFilterOp } from './types'; import { logger } from './utils/logger'; @@ -718,3 +719,20 @@ describe('calculateVisibleElementData', () => { }); }); }); + +describe('formatTemplate', () => { + it('should replace placeholders with provided values', () => { + expect( + formatTemplate('Hello {name}, you are {age} years old', { name: 'John', age: 30 }), + ).toBe('Hello John, you are 30 years old'); + expect( + formatTemplate('Expected {type}, but received {actual}', { + type: 'string', + actual: 'number', + }), + ).toBe('Expected string, but received number'); + expect( + formatTemplate('Hello {name}, you are {age} years old', { name: 'John' }), + ).toBe('Hello John, you are {age} years old'); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index af71550a..160bad71 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -499,3 +499,24 @@ export const calculateVisibleElementData = (element: HTMLElement) => { return data; } + +/** + * Replaces placeholders in a template string with provided values. + * Placeholders should be in the format {key}. + * @param template - The template string with placeholders + * @param values - An object containing key-value pairs to replace placeholders + * @returns The template string with placeholders replaced + * @example + * formatTemplate('Hello {name}, you are {age} years old', { name: 'John', age: 30 }) + * // Returns: 'Hello John, you are 30 years old' + * + * formatTemplate('Expected {type}, but received {actual}', { type: 'string', actual: 'number' }) + * // Returns: 'Expected string, but received number' + */ +export const formatTemplate = (template: string, values: Record): string => { + // This regex /\{(\w+)\}/g finds all placeholders in the format {word} + // and captures the word inside the braces for replacement. + return template.replace(/\{(\w+)\}/g, (match, key) => { + return values[key] !== undefined ? String(values[key]) : match; + }); +}; \ No newline at end of file