From 66f0b01d69556261570e70f739c069de720fb746 Mon Sep 17 00:00:00 2001 From: Ramya Anusri <62586490+ramya18101@users.noreply.github.com> Date: Mon, 9 Dec 2024 19:00:25 +0530 Subject: [PATCH] Add support for rendering the prompt-screen's settings (#998) --- package-lock.json | 14 +- package.json | 2 +- src/context/directory/handlers/prompts.ts | 41 +++++- src/context/yaml/handlers/prompts.ts | 117 +++++++++++++++- src/tools/auth0/handlers/prompts.ts | 152 ++++++++++++++++++++- src/tools/auth0/handlers/triggers.ts | 4 +- src/tools/constants.ts | 17 +++ test/context/directory/prompts.test.ts | 10 +- test/context/yaml/context.test.js | 3 + test/context/yaml/prompts.test.ts | 83 ++++++++++- test/tools/auth0/handlers/prompts.tests.ts | 68 ++++++++- test/utils.js | 1 + 12 files changed, 484 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 67a1f7d54..352ca2df4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "ajv": "^6.12.6", - "auth0": "^4.14.0", + "auth0": "^4.15.0", "dot-prop": "^5.2.0", "fs-extra": "^10.1.0", "global-agent": "^2.1.12", @@ -1404,9 +1404,9 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "node_modules/auth0": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.14.0.tgz", - "integrity": "sha512-SJRw0wgjAR9I4MTtU0GsjIw9daYI5XB98BlVf6bn2WJ+X1TqN6tnuuDzz+aQd3lEzAb5B9MEkIJSiirTGo7pPQ==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.15.0.tgz", + "integrity": "sha512-GPJJcDxk0sMnpbTZf9QWHXJrdPoSTG9eG5q9Un7X+eNKhZVPEzr3sSK3/EToRP4fNIPKbCWjXa2+HM3GnY3F4g==", "license": "MIT", "dependencies": { "jose": "^4.13.2", @@ -7617,9 +7617,9 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "auth0": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.14.0.tgz", - "integrity": "sha512-SJRw0wgjAR9I4MTtU0GsjIw9daYI5XB98BlVf6bn2WJ+X1TqN6tnuuDzz+aQd3lEzAb5B9MEkIJSiirTGo7pPQ==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.15.0.tgz", + "integrity": "sha512-GPJJcDxk0sMnpbTZf9QWHXJrdPoSTG9eG5q9Un7X+eNKhZVPEzr3sSK3/EToRP4fNIPKbCWjXa2+HM3GnY3F4g==", "requires": { "jose": "^4.13.2", "undici-types": "^6.15.0", diff --git a/package.json b/package.json index 08a6c1dec..9a4c004df 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "homepage": "https://github.com/auth0/auth0-deploy-cli#readme", "dependencies": { "ajv": "^6.12.6", - "auth0": "^4.14.0", + "auth0": "^4.15.0", "dot-prop": "^5.2.0", "fs-extra": "^10.1.0", "global-agent": "^2.1.12", diff --git a/src/context/directory/handlers/prompts.ts b/src/context/directory/handlers/prompts.ts index 0d24ebd30..039bc15a2 100644 --- a/src/context/directory/handlers/prompts.ts +++ b/src/context/directory/handlers/prompts.ts @@ -1,7 +1,7 @@ import path from 'path'; import { ensureDirSync, writeFileSync } from 'fs-extra'; import { constants, loadFileAndReplaceKeywords } from '../../../tools'; -import { dumpJSON, existsMustBeDir, isFile, loadJSON } from '../../../utils'; +import { getFiles, dumpJSON, existsMustBeDir, isFile, loadJSON } from '../../../utils'; import { DirectoryHandler } from '.'; import DirectoryContext from '..'; import { ParsedAsset } from '../../../types'; @@ -15,6 +15,7 @@ import { Prompts, PromptSettings, ScreenConfig, + ScreenRenderer, } from '../../../tools/auth0/handlers/prompts'; type ParsedPrompts = ParsedAsset<'prompts', Prompts>; @@ -29,6 +30,9 @@ const getCustomTextFile = (promptsDirectory: string) => const getPartialsFile = (promptsDirectory: string) => path.join(promptsDirectory, 'partials.json'); +const getScreenRenderSettingsDir = (promptsDirectory: string) => + path.join(promptsDirectory, constants.PROMPTS_SCREEN_RENDER_DIRECTORY); + function parse(context: DirectoryContext): ParsedPrompts { const promptsDirectory = getPromptsDirectory(context.filePath); if (!existsMustBeDir(promptsDirectory)) return { prompts: null }; // Skip @@ -84,11 +88,31 @@ function parse(context: DirectoryContext): ParsedPrompts { }, {} as Record>>); })(); + const screenRenderers = (() => { + const screenRenderSettingsDir = getScreenRenderSettingsDir(promptsDirectory); + if (!existsMustBeDir(screenRenderSettingsDir)) return []; + + const screenSettingsFiles = getFiles(screenRenderSettingsDir, ['.json']); + + const renderSettings: ScreenRenderer[] = screenSettingsFiles.map((f) => { + const renderSetting = { + ...loadJSON(f, { + mappings: context.mappings, + disableKeywordReplacement: context.disableKeywordReplacement, + }), + }; + return renderSetting as ScreenRenderer; + }); + + return renderSettings as ScreenRenderer[]; + })(); + return { prompts: { ...promptsSettings, customText, partials, + screenRenderers, }, }; } @@ -98,7 +122,7 @@ async function dump(context: DirectoryContext): Promise { if (!prompts) return; - const { customText, partials, ...promptsSettings } = prompts; + const { customText, partials, screenRenderers, ...promptsSettings } = prompts; const promptsDirectory = getPromptsDirectory(context.filePath); ensureDirSync(promptsDirectory); @@ -144,6 +168,19 @@ async function dump(context: DirectoryContext): Promise { }, {} as CustomPartialsConfig); dumpJSON(partialsFile, transformedPartials); + + if (!screenRenderers) return; + const screenRenderSettingsDir = getScreenRenderSettingsDir(promptsDirectory); + ensureDirSync(screenRenderSettingsDir); + + for (let index = 0; index < screenRenderers.length; index++) { + const screenRenderersSetting = screenRenderers[index]; + delete screenRenderersSetting.tenant; + const fileName = `${screenRenderersSetting.prompt}_${screenRenderersSetting.screen}.json`; + const screenSettingsFilePath = path.join(screenRenderSettingsDir, fileName); + + dumpJSON(screenSettingsFilePath, screenRenderersSetting); + } } const promptsHandler: DirectoryHandler = { diff --git a/src/context/yaml/handlers/prompts.ts b/src/context/yaml/handlers/prompts.ts index 11951e1bc..aa20e0f62 100644 --- a/src/context/yaml/handlers/prompts.ts +++ b/src/context/yaml/handlers/prompts.ts @@ -1,23 +1,132 @@ +import path from 'path'; +import { ensureDirSync, writeFileSync } from 'fs-extra'; +import { GetRendering200Response } from 'auth0'; import { YAMLHandler } from '.'; import YAMLContext from '..'; +import { constants } from '../../../tools'; import { ParsedAsset } from '../../../types'; -import { Prompts } from '../../../tools/auth0/handlers/prompts'; +import { Prompts, ScreenRenderer } from '../../../tools/auth0/handlers/prompts'; +import { loadJSON } from '../../../utils'; +import log from '../../../logger'; type ParsedPrompts = ParsedAsset<'prompts', Prompts>; +// Type for the screen render array +type ScreenRenderYAML = Array<{ + [prompt: string]: { + [screen: string]: string; // filename + }; +}>; + +const getPromptsDirectory = (filePath: string) => path.join(filePath, constants.PROMPTS_DIRECTORY); + +const loadScreenRenderers = ( + context: YAMLContext, + screenRenderArray: ScreenRenderYAML +): GetRendering200Response[] => { + // Array to store loaded renderers + const loadedRenderers: GetRendering200Response[] = []; + + screenRenderArray.forEach((promptEntry) => { + // Get the prompt (there will be only one key in each entry) + const prompt = Object.keys(promptEntry)[0]; + + const screens = promptEntry[prompt]; + + Object.entries(screens).forEach(([, fileName]) => { + const filePath = fileName; + + try { + const rendererFile = path.join(context.basePath, filePath); + + const rendererData = loadJSON(rendererFile, { + mappings: context.mappings, + disableKeywordReplacement: context.disableKeywordReplacement, + }); + + // Add to the loadedRenderers array + loadedRenderers.push(rendererData); + } catch (error) { + log.error(`Error loading file ${fileName}:`, error); + } + }); + }); + + return loadedRenderers; +}; + +async function parse(context: YAMLContext): Promise { + const { prompts } = context.assets; + if (!prompts) return { prompts: null }; + + if (prompts.screenRenderers && prompts.screenRenderers.length > 0) { + const screenRendersYAML = prompts.screenRenderers as ScreenRenderYAML; + prompts.screenRenderers = loadScreenRenderers(context, screenRendersYAML); + } + + return { + prompts, + }; +} + +const dumpScreenRenderers = (context: YAMLContext, screenRenderers: ScreenRenderer[]) => { + const screenRenderArray: ScreenRenderYAML = []; + + const promptsDirectory = getPromptsDirectory(context.basePath); + ensureDirSync(promptsDirectory); + + // Create the directory for render settings if it doesn't exist + const renderSettingsDir = path.join(promptsDirectory, constants.PROMPTS_SCREEN_RENDER_DIRECTORY); + ensureDirSync(renderSettingsDir); + + screenRenderers.forEach((renderer) => { + const { tenant, ...screenRendererConfig } = renderer; + if (!renderer.prompt || !renderer.screen) { + log.error('Invalid screen renderer:', renderer); + return; + } + const fileName = `${renderer.prompt}_${renderer.screen}.json`; + const filePath = path.join(renderSettingsDir, fileName); -async function parseAndDump(context: YAMLContext): Promise { + log.info(`Writing ${filePath}`); + + // Write individual file + writeFileSync(filePath, JSON.stringify(screenRendererConfig, null, 2)); + + // Find or create entry for this prompt in the screenRenderArray + let promptEntry = screenRenderArray.find((entry) => entry[renderer.prompt as string]); + + if (!promptEntry) { + // If no entry exists for this prompt, create a new one + promptEntry = { [renderer.prompt as string]: {} }; + screenRenderArray.push(promptEntry); + } + + // Add screen to the prompt entry + promptEntry[renderer.prompt as string][ + renderer.screen as string + ] = `./prompts/${constants.PROMPTS_SCREEN_RENDER_DIRECTORY}/${fileName}`; + }); + + return screenRenderArray; +}; + +async function dump(context: YAMLContext): Promise { const { prompts } = context.assets; if (!prompts) return { prompts: null }; + if (prompts.screenRenderers && prompts.screenRenderers.length > 0) { + prompts.screenRenderers = dumpScreenRenderers(context, prompts.screenRenderers); + } + return { prompts, }; } const promptsHandler: YAMLHandler = { - parse: parseAndDump, - dump: parseAndDump, + parse, + dump, }; export default promptsHandler; diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index ef0f31721..5aebd76b5 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -1,6 +1,14 @@ import { isEmpty } from 'lodash'; -import { GetPartialsPromptEnum, PutPartialsRequest } from 'auth0'; +import { + GetPartialsPromptEnum, + GetRendering200Response, + GetRenderingScreenEnum, + PatchRenderingRequest, + PatchRenderingRequestRenderingModeEnum, + PutPartialsRequest, +} from 'auth0'; import DefaultHandler from './default'; +import constants from '../../constants'; import { Assets, Language, languages } from '../../../types'; import log from '../../../logger'; @@ -91,6 +99,9 @@ const screenTypes = [ 'redeem-ticket', 'organization-selection', 'accept-invitation', + 'login-passwordless-email-code', + 'login-passwordless-email-link', + 'login-passwordless-sms-otp', ] as const; export type ScreenTypes = typeof screenTypes[number]; @@ -154,6 +165,17 @@ export type CustomPartialsConfig = { ]; }; +export type PromptScreenRenderSettings = { + name: string; + body: string; +}; + +export type PromptScreenSettings = { + [prompt in PromptTypes]: Partial<{ + [screen in ScreenTypes]: PromptScreenRenderSettings; + }>; +}; + export const schema = { type: 'object', properties: { @@ -237,6 +259,27 @@ export const schema = { {} ), }, + screenRenderers: { + type: 'array', + properties: promptTypes.reduce( + (promptAcc, promptType) => ({ + ...promptAcc, + [promptType]: { + type: 'array', + properties: screenTypes.reduce( + (screenAcc, screenType) => ({ + ...screenAcc, + [screenType]: { + type: 'string', + }, + }), + {} + ), + }, + }), + {} + ), + }, }, }; @@ -258,10 +301,13 @@ export type AllPromptsByLanguage = Partial<{ [key in Language]: Partial; }>; +export type ScreenRenderer = Partial; + export type Prompts = Partial< PromptSettings & { customText: AllPromptsByLanguage; partials: CustomPromptPartials; + screenRenderers?: ScreenRenderer[]; } >; @@ -277,8 +323,15 @@ export default class PromptsHandler extends DefaultHandler { }); } - objString({ customText }: Prompts): string { - return `Prompts settings${customText ? ' and prompts custom text' : ''}`; + objString({ customText, screenRenderers }: Prompts): string { + let description = 'Prompts settings'; + if (customText) { + description += ' and prompts custom text'; + } + if (screenRenderers && screenRenderers.length > 0) { + description += ' and screen renderers'; + } + return description; } async getType(): Promise { @@ -288,11 +341,20 @@ export default class PromptsHandler extends DefaultHandler { const partials = await this.getCustomPromptsPartials(); - return { + const prompts: Prompts = { ...promptsSettings, customText, partials, }; + + try { + const screenRenderers = await this.getPromptScreenSettings(); + prompts.screenRenderers = screenRenderers; + } catch (error) { + log.warn(`Unable to fetch screen renderers: ${error}`); + } + + return prompts; } async getCustomTextSettings(): Promise { @@ -419,12 +481,44 @@ export default class PromptsHandler extends DefaultHandler { ); } + async getPromptScreenSettings(): Promise { + log.info('Loading Prompt Screen Renderers. This may take a while...'); + + // Create combinations of prompt and screens + const promptScreenCombinations = Object.entries(constants.PROMPT_SCREEN_MAPPINGS).flatMap( + ([promptType, screens]) => + screens.map((screen) => ({ + promptName: promptType, + screenName: screen, + })) + ); + + const renderSettings = await this.client.pool + .addEachTask({ + data: promptScreenCombinations, + generator: ({ promptName, screenName }) => + this.client.prompts + .getRendering({ + prompt: promptName as GetPartialsPromptEnum, + screen: screenName as GetRenderingScreenEnum, + }) + .then(({ data: renderingSettings }) => { + if (isEmpty(renderingSettings)) return null; + return renderingSettings; + }), + }) + .promise(); + + const customRenderingRes = renderSettings.filter((item) => item !== null); + return customRenderingRes; + } + async processChanges(assets: Assets): Promise { const { prompts } = assets; if (!prompts) return; - const { partials, customText, ...promptSettings } = prompts; + const { partials, customText, screenRenderers, ...promptSettings } = prompts; if (!isEmpty(promptSettings)) { await this.client.prompts.update(promptSettings); @@ -433,6 +527,9 @@ export default class PromptsHandler extends DefaultHandler { await this.updateCustomTextSettings(customText); await this.updateCustomPromptsPartials(partials); + // Update screen renderers + await this.updateScreenRenderers(screenRenderers); + this.updated += 1; this.didUpdate(prompts); } @@ -496,4 +593,49 @@ export default class PromptsHandler extends DefaultHandler { }) .promise(); } + + async updateScreenRenderer(screenRenderer: ScreenRenderer): Promise { + const { prompt, screen, rendering_mode, tenant, ...updatePrams } = screenRenderer; + + if (!prompt || !screen) return; + + let updatePayload: PatchRenderingRequest = {}; + + if (rendering_mode === PatchRenderingRequestRenderingModeEnum.standard) { + updatePayload = { + rendering_mode, + }; + } else { + updatePayload = { + ...updatePrams, + }; + } + + try { + await this.client.prompts.updateRendering( + { + prompt: prompt as GetPartialsPromptEnum, + screen: screen as GetRenderingScreenEnum, + }, + { + ...updatePayload, + } + ); + } catch (error) { + const errorMessage = `Problem updating ${this.type} screen renderers ${prompt}:${screen}\n${error}`; + log.error(errorMessage); + throw new Error(errorMessage); + } + } + + async updateScreenRenderers(screenRenderers: Prompts['screenRenderers']): Promise { + if (isEmpty(screenRenderers) || !screenRenderers) return; + + await this.client.pool + .addEachTask({ + data: screenRenderers, + generator: (updateParams) => this.updateScreenRenderer(updateParams), + }) + .promise(); + } } diff --git a/src/tools/auth0/handlers/triggers.ts b/src/tools/auth0/handlers/triggers.ts index 356e02954..6395d60a6 100644 --- a/src/tools/auth0/handlers/triggers.ts +++ b/src/tools/auth0/handlers/triggers.ts @@ -70,9 +70,7 @@ export default class TriggersHandler extends DefaultHandler { bindings = data?.bindings; } catch (err) { - log.warn( - `${err.message} (trigger: ${triggerId}). Skipping this trigger and continuing.` - ); + log.warn(`${err.message} (trigger: ${triggerId}). Skipping this trigger and continuing.`); continue; } diff --git a/src/tools/constants.ts b/src/tools/constants.ts index 204e2b966..ce37b53e4 100644 --- a/src/tools/constants.ts +++ b/src/tools/constants.ts @@ -24,6 +24,10 @@ const UNIVERSAL_LOGIN_TEMPLATE = 'universal_login'; const OBFUSCATED_SECRET_VALUE = '_VALUE_NOT_SHOWN_'; +type PromptScreenMapping = { + [prompt: string]: string[]; // Array of screens for each prompt +}; + const constants = { CONCURRENT_CALLS: 5, RULES_DIRECTORY: 'rules', @@ -171,6 +175,7 @@ const constants = { SUPPORTED_BRANDING_TEMPLATES: [UNIVERSAL_LOGIN_TEMPLATE], LOG_STREAMS_DIRECTORY: 'log-streams', PROMPTS_DIRECTORY: 'prompts', + PROMPTS_SCREEN_RENDER_DIRECTORY: 'screenRenderSettings', PARTIALS_DIRECTORY: 'partials', CUSTOM_DOMAINS_DIRECTORY: 'custom-domains', THEMES_DIRECTORY: 'themes', @@ -201,6 +206,18 @@ const constants = { FLOWS_DIRECTORY: 'flows', FLOWS_VAULT_DIRECTORY: 'flow-vault-connections', SELF_SERVICE_PROFILE_DIRECTORY: 'self-service-profiles', + PROMPT_SCREEN_MAPPINGS: { + 'signup-id': ['signup-id'], + 'signup-password': ['signup-password'], + 'login-id': ['login-id'], + 'login-password': ['login-password'], + 'login-passwordless': ['login-passwordless-email-code', 'login-passwordless-sms-otp'], + 'phone-identifier-enrollment': ['phone-identifier-enrollment'], + 'phone-identifier-challenge': ['phone-identifier-challenge'], + 'email-identifier-challenge': ['email-identifier-challenge'], + passkeys: ['passkey-enrollment', 'passkey-enrollment-local'], + captcha: ['interstitial-captcha'], + } as PromptScreenMapping, }; export default constants; diff --git a/test/context/directory/prompts.test.ts b/test/context/directory/prompts.test.ts index cc6f13a7b..f99c48673 100644 --- a/test/context/directory/prompts.test.ts +++ b/test/context/directory/prompts.test.ts @@ -149,6 +149,7 @@ describe('#directory context prompts', () => { }, }, }, + screenRenderers: [], }); }); @@ -252,6 +253,7 @@ describe('#directory context prompts', () => { }, }, }, + screenRenderers: [], }); }); @@ -280,6 +282,7 @@ describe('#directory context prompts', () => { ...mockPromptsSettings, customText: {}, partials: {}, + screenRenderers: [], }); }); @@ -308,6 +311,7 @@ describe('#directory context prompts', () => { ...mockPromptsSettings, customText: {}, partials: {}, + screenRenderers: [], }); }); @@ -325,7 +329,11 @@ describe('#directory context prompts', () => { const context = new Context(config, mockMgmtClient()); await context.loadAssetsFromLocal(); - expect(context.assets.prompts).to.deep.equal({ customText: {}, partials: {} }); + expect(context.assets.prompts).to.deep.equal({ + customText: {}, + partials: {}, + screenRenderers: [], + }); }); }); diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 88ab97198..6ac28f747 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -292,6 +292,7 @@ describe('#YAML context validation', () => { prompts: { customText: {}, partials: {}, + screenRenderers: [], }, customDomains: [], themes: [], @@ -408,6 +409,7 @@ describe('#YAML context validation', () => { prompts: { customText: {}, partials: {}, + screenRenderers: [], }, customDomains: [], themes: [], @@ -524,6 +526,7 @@ describe('#YAML context validation', () => { prompts: { customText: {}, partials: {}, + screenRenderers: [], }, logStreams: [], customDomains: [], diff --git a/test/context/yaml/prompts.test.ts b/test/context/yaml/prompts.test.ts index 5783075e7..82775a50f 100644 --- a/test/context/yaml/prompts.test.ts +++ b/test/context/yaml/prompts.test.ts @@ -71,10 +71,19 @@ describe('#YAML context prompts', () => { login-id: form-content-end: >-
TEST
+ screenRenderers: + - signup-id: + signup-id: ./screenRenderSettings/signup-id_signup-id.json `; const yamlFile = path.join(dir, 'config.yaml'); fs.writeFileSync(yamlFile, yaml); + const screenRendererPath = path.join(dir, 'screenRenderSettings'); + fs.ensureDirSync(screenRendererPath); + fs.writeFileSync( + path.join(screenRendererPath, 'signup-id_signup-id.json'), + '{"prompt":"signup-id","screen":"signup-id","rendering_mode":"standard","context_configuration":[],"default_head_tags_disabled":false,"head_tags":[{"tag":"script","attributes":{"src":"URL_TO_YOUR_ASSET","async":true,"defer":true,"integrity":["ASSET_SHA"]}}]}' + ); const config = { AUTH0_INPUT_FILE: yamlFile }; const context = new Context(config, mockMgmtClient()); @@ -152,6 +161,26 @@ describe('#YAML context prompts', () => { }, }, universal_login_experience: 'classic', + screenRenderers: [ + { + prompt: 'signup-id', + screen: 'signup-id', + rendering_mode: 'standard', + context_configuration: [], + default_head_tags_disabled: false, + head_tags: [ + { + tag: 'script', + attributes: { + src: 'URL_TO_YOUR_ASSET', + async: true, + defer: true, + integrity: ['ASSET_SHA'], + }, + }, + ], + }, + ], }); }); @@ -223,8 +252,14 @@ describe('#YAML context prompts', () => { }); }); - it('should dump prompts settings and prompts custom text', async () => { - const context = new Context({ AUTH0_INPUT_FILE: './test.yml' }, mockMgmtClient()); + it('should dump prompts settings, prompts custom text and screen renderers', async () => { + const dir = path.join(testDataDir, 'yaml', 'prompts'); + cleanThenMkdir(dir); + const context = new Context( + { AUTH0_INPUT_FILE: path.join(dir, './test.yml') }, + mockMgmtClient() + ); + context.assets.prompts = { universal_login_experience: 'classic', identifier_first: true, @@ -272,9 +307,53 @@ describe('#YAML context prompts', () => { }, }, }, + screenRenderers: [ + { + prompt: 'signup-id', + screen: 'signup-id', + rendering_mode: 'standard', + context_configuration: [], + default_head_tags_disabled: false, + head_tags: [ + { + tag: 'script', + attributes: { + src: 'URL_TO_YOUR_ASSET', + async: true, + defer: true, + integrity: ['ASSET_SHA'], + }, + }, + ], + }, + ], }; + const renderSettingsFolder = path.join(dir, 'prompts', 'screenRenderSettings'); + const dumped = await promptsHandler.dump(context); expect(dumped).to.deep.equal({ prompts: context.assets.prompts }); + expect( + JSON.parse( + fs.readFileSync(path.join(renderSettingsFolder, 'signup-id_signup-id.json'), 'utf8') + ) + ).to.deep.equal({ + prompt: 'signup-id', + screen: 'signup-id', + rendering_mode: 'standard', + context_configuration: [], + default_head_tags_disabled: false, + head_tags: [ + { + tag: 'script', + attributes: { + src: 'URL_TO_YOUR_ASSET', + async: true, + defer: true, + integrity: ['ASSET_SHA'], + }, + }, + ], + }); }); }); diff --git a/test/tools/auth0/handlers/prompts.tests.ts b/test/tools/auth0/handlers/prompts.tests.ts index b93071149..b63e9a1a1 100644 --- a/test/tools/auth0/handlers/prompts.tests.ts +++ b/test/tools/auth0/handlers/prompts.tests.ts @@ -14,7 +14,7 @@ const mockPromptsSettings = { describe('#prompts handler', () => { describe('#prompts process', () => { - it('should get prompts settings, custom texts and template partials', async () => { + it('should get prompts settings, custom texts, template partials and screen renderer', async () => { const supportedLanguages: Language[] = ['es', 'fr', 'en']; const englishCustomText = { @@ -61,6 +61,31 @@ describe('#prompts handler', () => { 'form-content-end': '
TEST
', }, }; + + const sampleScreenRenderSignUp = { + prompt: 'signup', + screen: 'signup', + rendering_mode: 'standard', + }; + + const sampleScreenRenderLogin = { + prompt: 'login', + screen: 'login', + rendering_mode: 'advanced', + default_head_tags_disabled: false, + head_tags: [ + { + tag: 'script', + attributes: { + src: 'URL_TO_YOUR_ASSET', + async: true, + defer: true, + integrity: ['ASSET_SHA'], + }, + }, + ], + }; + const auth0 = { tenants: { getSettings: () => @@ -106,6 +131,9 @@ describe('#prompts handler', () => { getCustomPartial.withArgs({ prompt: 'signup-id' }).resolves({}); getCustomPartial.withArgs({ prompt: 'signup' }).resolves({ data: signupPartial }); + const getPromptScreenSettings = sinon.stub(handler, 'getPromptScreenSettings'); + getPromptScreenSettings.resolves([sampleScreenRenderLogin, sampleScreenRenderSignUp]); + const data = await handler.getType(); expect(data).to.deep.equal({ ...mockPromptsSettings, @@ -127,6 +155,7 @@ describe('#prompts handler', () => { signup: signupPartial.signup, }, }, + screenRenderers: [sampleScreenRenderLogin, sampleScreenRenderSignUp], }); }); @@ -174,12 +203,13 @@ describe('#prompts handler', () => { expect(didCallUpdatePartials).to.equal(false); }); - it('should update prompts settings and custom text/partials settings when set', async () => { + it('should update prompts settings and custom text/partials, screen renderer settings when set', async () => { let didCallUpdatePromptsSettings = false; let didCallUpdateCustomText = false; let didCallUpdatePartials = false; let numberOfUpdateCustomTextCalls = 0; let numberOfUpdatePartialsCalls = 0; + let didCallUpdateScreenRenderer = false; const customTextToSet = { en: { @@ -216,6 +246,26 @@ describe('#prompts handler', () => { }, }, }; + const screenRenderersToSet: Prompts['screenRenderers'] = [ + { + prompt: 'login', + screen: 'login', + rendering_mode: 'advanced', + context_configuration: ['branding.settings', 'branding.themes.default'], + default_head_tags_disabled: false, + head_tags: [ + { + tag: 'script', + attributes: { + src: 'URL_TO_YOUR_ASSET', + async: true, + defer: true, + integrity: ['ASSET_SHA'], + }, + }, + ], + }, + ]; const auth0 = { prompts: { @@ -229,6 +279,10 @@ describe('#prompts handler', () => { expect(data).to.deep.equal(mockPromptsSettings); return Promise.resolve({ data }); }, + updateRendering: () => { + didCallUpdateScreenRenderer = true; + return Promise.resolve({ data: {} }); + }, _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), }), @@ -254,12 +308,18 @@ describe('#prompts handler', () => { await stageFn.apply(handler, [ { - prompts: { ...mockPromptsSettings, customText: customTextToSet, partials: partialsToSet }, + prompts: { + ...mockPromptsSettings, + customText: customTextToSet, + partials: partialsToSet, + screenRenderers: screenRenderersToSet, + }, }, ]); expect(didCallUpdatePromptsSettings).to.equal(true); expect(didCallUpdateCustomText).to.equal(true); expect(didCallUpdatePartials).to.equal(true); + expect(didCallUpdateScreenRenderer).to.equal(true); expect(numberOfUpdateCustomTextCalls).to.equal(3); expect(numberOfUpdatePartialsCalls).to.equal(3); }); @@ -280,6 +340,7 @@ describe('#prompts handler', () => { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), }), + getRendering: () => Promise.resolve({ data: {} }), }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -303,6 +364,7 @@ describe('#prompts handler', () => { ...mockPromptsSettings, customText: {}, // Custom text empty partials: {}, // Partials empty + screenRenderers: [], }); }); }); diff --git a/test/utils.js b/test/utils.js index 29f4883f8..4017a9781 100644 --- a/test/utils.js +++ b/test/utils.js @@ -129,6 +129,7 @@ export function mockMgmtClient() { res({ data: {} }); }), get: () => ({ data: {} }), + getRendering: () => Promise.resolve({ data: {} }), }, customDomains: { getAll: (params) => mockPagedData(params, 'custom_domains', []) }, forms: { getAll: (params) => mockPagedData(params, 'forms', []) },