From cd37290c886c0d500125cbabf9c46c7830452e5a Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 00:03:08 +0530 Subject: [PATCH 01/20] Add Support for Custom Prompts --- src/context/directory/handlers/prompts.ts | 76 +++++- src/tools/auth0/handlers/prompts.ts | 279 ++++++++++++++++--- src/tools/constants.ts | 1 + test/context/directory/prompts.test.ts | 91 ++++++- test/context/yaml/context.test.js | 18 +- test/context/yaml/prompts.test.ts | 33 +++ test/tools/auth0/handlers/prompts.tests.ts | 299 +++++++++++++++++++-- test/utils.js | 3 +- 8 files changed, 710 insertions(+), 90 deletions(-) diff --git a/src/context/directory/handlers/prompts.ts b/src/context/directory/handlers/prompts.ts index 12fee4eae..5ba88946d 100644 --- a/src/context/directory/handlers/prompts.ts +++ b/src/context/directory/handlers/prompts.ts @@ -1,28 +1,29 @@ import path from 'path'; -import { ensureDirSync } from 'fs-extra'; +import { ensureDirSync, readFileSync, writeFileSync } from 'fs-extra'; import { constants } from '../../../tools'; -import { existsMustBeDir, dumpJSON, loadJSON, isFile } from '../../../utils'; +import { dumpJSON, existsMustBeDir, isFile, loadJSON } from '../../../utils'; import { DirectoryHandler } from '.'; import DirectoryContext from '..'; import { ParsedAsset } from '../../../types'; import { - Prompts, - PromptSettings, AllPromptsByLanguage, + CustomPartialsConfig, + CustomPartialsPromptTypes, + Prompts, + PromptSettings } from '../../../tools/auth0/handlers/prompts'; +import log from '../../../logger'; type ParsedPrompts = ParsedAsset<'prompts', Prompts>; -const getPromptsDirectory = (filePath: string) => { - return path.join(filePath, constants.PROMPTS_DIRECTORY); -}; +const getPromptsDirectory = (filePath: string) => path.join(filePath, constants.PROMPTS_DIRECTORY); -const getPromptsSettingsFile = (promptsDirectory: string) => { - return path.join(promptsDirectory, 'prompts.json'); -}; +const getPromptsSettingsFile = (promptsDirectory: string) => path.join(promptsDirectory, 'prompts.json'); + +const getCustomTextFile = (promptsDirectory: string) => path.join(promptsDirectory, 'custom-text.json'); -const getCustomTextFile = (promptsDirectory: string) => { - return path.join(promptsDirectory, 'custom-text.json'); +const getPartialsFile = (promptsDirectory: string) => { + return path.join(promptsDirectory, 'partials.json'); }; function parse(context: DirectoryContext): ParsedPrompts { @@ -47,10 +48,40 @@ function parse(context: DirectoryContext): ParsedPrompts { }) as AllPromptsByLanguage; })(); + const partials = (() => { + const partialsFile = getPartialsFile(promptsDirectory); + if (!isFile(partialsFile)) return {}; + const partialsFileContent = loadJSON(partialsFile, { + mappings: context.mappings, + disableKeywordReplacement: context.disableKeywordReplacement, + }) as CustomPartialsConfig; + + return Object.entries(partialsFileContent).reduce((acc, [promptName, items]) => { + acc[promptName] = items.reduce((screenAcc, { name, template }) => { + if (!screenAcc[promptName]) { + screenAcc[promptName] = {}; + } + + // Read template content from the file + const templateFilePath = path.join(promptsDirectory, template); + if (isFile(templateFilePath)) { + const templateContent = readFileSync(templateFilePath, 'utf8'); + if (templateContent.trim()) { + screenAcc[promptName][name] = templateContent; + } + } + + return screenAcc; + }, {} as Record>); + return acc; + }, {} as Record>>); + })(); + return { prompts: { ...promptsSettings, customText, + partials, }, }; } @@ -60,7 +91,7 @@ async function dump(context: DirectoryContext): Promise { if (!prompts) return; - const { customText, ...promptsSettings } = prompts; + const { customText, partials, ...promptsSettings } = prompts; const promptsDirectory = getPromptsDirectory(context.filePath); ensureDirSync(promptsDirectory); @@ -72,6 +103,25 @@ async function dump(context: DirectoryContext): Promise { if (!customText) return; const customTextFile = getCustomTextFile(promptsDirectory); dumpJSON(customTextFile, customText); + + if (!partials) return; + const partialsFile = getPartialsFile(promptsDirectory); + + // Transform the partials data back to CustomPartialsConfig format + const transformedPartials = Object.entries(partials).reduce((acc, [promptName, screens]) => { + acc[promptName] = Object.entries(screens).map(([, insertionPoints]) => Object.entries(insertionPoints).map(([insertionPoint, template]) => { + const templateFilePath = path.join(promptsDirectory, 'partials', promptName, `${insertionPoint}.liquid`); + ensureDirSync(path.dirname(templateFilePath)); + writeFileSync(templateFilePath, template, 'utf8'); + return { + name: insertionPoint, + template: path.relative(promptsDirectory, templateFilePath) // Path relative to `promptsDirectory` + }; + })).flat(); // Flatten the nested arrays into a single array + return acc; + }, {} as CustomPartialsConfig); + + dumpJSON(partialsFile, transformedPartials); } const promptsHandler: DirectoryHandler = { diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index b98ce7335..71d9df047 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -1,6 +1,8 @@ +import { isEmpty } from 'lodash'; +import axios, { AxiosResponse } from 'axios'; import DefaultHandler from './default'; import { Assets, Language, languages } from '../../../types'; -import { isEmpty } from 'lodash'; +import log from '../../../logger'; const promptTypes = [ 'login', @@ -93,6 +95,58 @@ const screenTypes = [ export type ScreenTypes = typeof screenTypes[number]; +const customPartialsPromptTypes = [ + 'login', + 'login-id', + 'login-password', + 'signup', + 'signup-id', + 'signup-password', +] as const; + +export type CustomPartialsPromptTypes = typeof customPartialsPromptTypes[number]; + +const customPartialsScreenTypes = [ + 'login', + 'login-id', + 'login-password', + 'signup', + 'signup-id', + 'signup-password', +] as const; + +export type CustomPartialsScreenTypes = typeof customPartialsPromptTypes[number]; + +const customPartialsInsertionPoints = [ + 'form-content-start', + 'form-content-end', + 'form-footer-start', + 'form-footer-end', + 'secondary-actions-start', + 'secondary-actions-end', +] as const; + +export type CustomPartialsInsertionPoints = typeof customPartialsInsertionPoints[number]; + +export type CustomPromptPartialsScreens = Partial<{ + [screen in CustomPartialsScreenTypes]: Partial<{ + [insertionPoint in CustomPartialsInsertionPoints]: string; + }>; +}>; + +export type CustomPromptPartials = Partial<{ + [prompt in CustomPartialsPromptTypes]: CustomPromptPartialsScreens; +}>; + +export type CustomPartialsConfig = { + [prompt in CustomPartialsPromptTypes]: [ + { + name: string; + template: string; + } + ]; +}; + export const schema = { type: 'object', properties: { @@ -108,31 +162,46 @@ export const schema = { }, customText: { type: 'object', - properties: languages.reduce((acc, language) => { - return { - ...acc, - [language]: { - type: 'object', - properties: promptTypes.reduce((acc, promptTypes) => { - return { + properties: languages.reduce((acc, language) => ({ + ...acc, + [language]: { + type: 'object', + properties: promptTypes.reduce((acc, customPartialsPromptTypes) => ({ + ...acc, + [customPartialsPromptTypes]: { + type: 'object', + properties: screenTypes.reduce((acc, screenTypes) => ({ ...acc, - [promptTypes]: { + [screenTypes]: { type: 'object', - properties: screenTypes.reduce((acc, screenTypes) => { - return { - ...acc, - [screenTypes]: { - type: 'object', - }, - }; - }, {}), }, - }; - }, {}), - }, - }; - }, {}), + }), {}), + }, + }), {}), + }, + }), {}), }, + partials: { + type: 'object', + properties: customPartialsPromptTypes.reduce((acc, customPartialsPromptTypes) => ({ + ...acc, + [customPartialsPromptTypes]: { + type: 'object', + properties: customPartialsScreenTypes.reduce((acc, customPartialsScreenTypes) => ({ + ...acc, + [customPartialsScreenTypes]: { + type: 'object', + properties: customPartialsInsertionPoints.reduce((acc, customPartialsInsertionPoints) => ({ + ...acc, + [customPartialsInsertionPoints]: { + type: 'string', + }, + }), {}), + }, + }), {}), + }, + }), {}), + } }, }; @@ -150,19 +219,36 @@ export type PromptsCustomText = { }>; }; +export type AllPromptsByLanguage = Partial<{ + [key in Language]: Partial; +}>; + export type Prompts = Partial< PromptSettings & { customText: AllPromptsByLanguage; + partials: CustomPromptPartials; } >; -export type AllPromptsByLanguage = Partial<{ - [key in Language]: Partial; -}>; - export default class PromptsHandler extends DefaultHandler { existing: Prompts; + private IsFeatureSupported: boolean = true; + + /** + * Returns formatted endpoint url. + */ + private getPartialsEndpoint(promptType: CustomPartialsPromptTypes) { + return `https://${this.config('AUTH0_DOMAIN')}/api/v2/prompts/${promptType}/partials`; + } + + /** + * Returns formatted endpoint url. + */ + private putPartialsEndpoint(promptType: CustomPartialsPromptTypes) { + return `https://${this.config('AUTH0_DOMAIN')}/api/v2/prompts/${promptType}/partials`; + } + constructor(options: DefaultHandler) { super({ ...options, @@ -171,7 +257,7 @@ export default class PromptsHandler extends DefaultHandler { } objString({ customText }: Prompts): string { - return `Prompts settings${!!customText ? ' and prompts custom text' : ''}`; + return `Prompts settings${customText ? ' and prompts custom text' : ''}`; } async getType(): Promise { @@ -179,9 +265,12 @@ export default class PromptsHandler extends DefaultHandler { const customText = await this.getCustomTextSettings(); + const partials = await this.getCustomPromptsPartials(); + return { ...promptsSettings, customText, + partials }; } @@ -216,24 +305,100 @@ export default class PromptsHandler extends DefaultHandler { }), }) .promise() - .then((customTextData) => { - return customTextData - .filter((customTextData) => { - return customTextData !== null; - }) - .reduce((acc: AllPromptsByLanguage, customTextItem) => { - if (customTextItem?.language === undefined) return acc; + .then((customTextData) => customTextData + .filter((customTextData) => customTextData !== null) + .reduce((acc: AllPromptsByLanguage, customTextItem) => { + if (customTextItem?.language === undefined) return acc; - const { language, ...customTextSettings } = customTextItem; + const { language, ...customTextSettings } = customTextItem; - return { - ...acc, - [language]: !!acc[language] - ? { ...acc[language], ...customTextSettings } - : { ...customTextSettings }, - }; - }, {}); - }); + return { + ...acc, + [language]: acc[language] + ? { ...acc[language], ...customTextSettings } + : { ...customTextSettings }, + }; + }, {})); + } + + private async partialHttpRequest(method: string, options: [string, ...Record[]]): Promise { + return this.withErrorHandling(async () => { + // @ts-ignore + const accessToken = await this.client.tokenProvider?.getAccessToken(); + const headers = { + 'Accept': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }; + options = [...options, { headers }]; + return axios[method](...options); + },); + } + + /** + * Error handler wrapper. + */ + async withErrorHandling(callback) { + try { + return await callback(); + } catch (error) { + + // Extract error data + const errorData = error?.response?.data; + if (errorData?.statusCode === 403) { + log.warn('Partial Prompts feature is not supported for the tenant'); + this.IsFeatureSupported = false; + return { data: null }; + } + + if(errorData?.statusCode === 400 && errorData?.message === 'This feature requires at least one custom domain to be configured for the tenant.') { + log.warn('Partial Prompts feature requires at least one custom domain to be configured for the tenant'); + this.IsFeatureSupported = false; + return { data: null }; + } + throw error; + } + } + + async getCustomPartial({ prompt }: { prompt: CustomPartialsPromptTypes }): Promise { + if (!this.IsFeatureSupported) return {}; + const url = this.getPartialsEndpoint(prompt); // Implement this method to return the correct endpoint URL + const response = await this.partialHttpRequest('get', [url]); // Implement this method for making HTTP requests + return response.data; + } + + async getCustomPromptsPartials(): Promise { + return this.client.pool + .addEachTask({ + data: customPartialsPromptTypes.map((promptType) => ({ promptType })), + generator: ({ promptType }) => + this.getCustomPartial({ + prompt: promptType, + }) + .then((partialsData: CustomPromptPartials) => { + if (isEmpty(partialsData)) return null; + return { + [promptType]: { + ...partialsData, + }, + }; + }), + }) + .promise() + .then((partialsDataWithNulls) => + partialsDataWithNulls + .filter(Boolean) + .reduce( + ( + acc: CustomPromptPartials, + partialsData: { [prompt: string]: CustomPromptPartials } + ) => { + const [promptName] = Object.keys(partialsData); + acc[promptName] = partialsData[promptName]; + return acc; + }, + {} + ) + ); } async processChanges(assets: Assets): Promise { @@ -241,13 +406,14 @@ export default class PromptsHandler extends DefaultHandler { if (!prompts) return; - const { customText, ...promptSettings } = prompts; + const { partials, customText, ...promptSettings } = prompts; if (!isEmpty(promptSettings)) { await this.client.prompts.updateSettings({}, promptSettings); } await this.updateCustomTextSettings(customText); + await this.updateCustomPromptsPartials(partials); this.updated += 1; this.didUpdate(prompts); @@ -284,4 +450,29 @@ export default class PromptsHandler extends DefaultHandler { }) .promise(); } + + async updateCustomPartials({ prompt, body }: { prompt: CustomPartialsPromptTypes; body: CustomPromptPartialsScreens }): Promise { + if (!this.IsFeatureSupported) return; + const url = this.putPartialsEndpoint(prompt); // Implement this method to return the correct endpoint URL + await this.partialHttpRequest('put', [url, body]); // Implement this method for making HTTP requests + } + + async updateCustomPromptsPartials(partials: Prompts['partials']): Promise { + /* + Note: deletes are not currently supported + */ + if (!partials) return; + await this.client.pool + .addEachTask({ + data: Object.keys(partials).map((prompt: CustomPartialsPromptTypes) => { + const body = partials[prompt] || {}; + return { + body, + prompt, + }; + }), + generator: ({ prompt, body }) => this.updateCustomPartials({ prompt ,body } ), + }) + .promise(); + } } diff --git a/src/tools/constants.ts b/src/tools/constants.ts index 4506b46fd..357e2fc91 100644 --- a/src/tools/constants.ts +++ b/src/tools/constants.ts @@ -169,6 +169,7 @@ const constants = { SUPPORTED_BRANDING_TEMPLATES: [UNIVERSAL_LOGIN_TEMPLATE], LOG_STREAMS_DIRECTORY: 'log-streams', PROMPTS_DIRECTORY: 'prompts', + PARTIALS_DIRECTORY: 'partials', CUSTOM_DOMAINS_DIRECTORY: 'custom-domains', THEMES_DIRECTORY: 'themes', }; diff --git a/test/context/directory/prompts.test.ts b/test/context/directory/prompts.test.ts index 295b8de38..5aa6047cf 100644 --- a/test/context/directory/prompts.test.ts +++ b/test/context/directory/prompts.test.ts @@ -12,6 +12,7 @@ const promptsDirectory = path.join(dir, constants.PROMPTS_DIRECTORY); const promptsSettingsFile = 'prompts.json'; const customTextFile = 'custom-text.json'; +const partialsFile = 'partials.json'; describe('#directory context prompts', () => { it('should parse prompts', async () => { @@ -45,12 +46,38 @@ describe('#directory context prompts', () => { }, }, }), + [partialsFile]: JSON.stringify({ + login: [ + { + name: 'form-content-start', + template: 'partials/login/form-content-start.liquid', + }, + ], + signup: [ + { + name: 'form-content-end', + template: 'partials/signup/form-content-end.liquid', + }, + ], + }), }, }; const repoDir = path.join(testDataDir, 'directory', 'prompts'); createDir(repoDir, files); + const partialsDir = path.join( + repoDir, + constants.PROMPTS_DIRECTORY, + constants.PARTIALS_DIRECTORY + ); + const partialsFiles = { + login: { 'form-content-start.liquid': '
TEST
' }, + signup: { 'form-content-end.liquid': '
TEST AGAIN
' }, + }; + + createDir(partialsDir, partialsFiles); + const config = { AUTH0_INPUT_FILE: repoDir, AUTH0_KEYWORD_REPLACE_MAPPINGS: { @@ -60,10 +87,21 @@ describe('#directory context prompts', () => { }; const context = new Context(config, mockMgmtClient()); await context.loadAssetsFromLocal(); - expect(context.assets.prompts).to.deep.equal({ universal_login_experience: 'classic', identifier_first: true, + partials: { + login: { + login:{ + 'form-content-start': '
TEST
', + } + }, + signup: { + signup: { + 'form-content-end': '
TEST AGAIN
', + }, + } + }, customText: { en: { login: { @@ -92,7 +130,7 @@ describe('#directory context prompts', () => { }); describe('should parse prompts even if one or both files are absent', async () => { - it('should parse even if custom text file is absent', async () => { + it('should parse prompts even if one or more files are absent', async () => { cleanThenMkdir(promptsDirectory); const mockPromptsSettings = { universal_login_experience: 'classic', @@ -101,18 +139,50 @@ describe('#directory context prompts', () => { const promptsDirectoryNoCustomTextFile = { [constants.PROMPTS_DIRECTORY]: { [promptsSettingsFile]: JSON.stringify(mockPromptsSettings), + [partialsFile]: JSON.stringify({}), }, }; createDir(promptsDirectory, promptsDirectoryNoCustomTextFile); + const config = { + AUTH0_INPUT_FILE: promptsDirectory, + }; + const context = new Context(config, mockMgmtClient()); + await context.loadAssetsFromLocal(); + expect(context.assets.prompts).to.deep.equal({ + ...mockPromptsSettings, + customText: {}, + partials: {}, + }); + }); + + it('should parse even if custom prompts file is absent', async () => { + cleanThenMkdir(promptsDirectory); + const mockPromptsSettings = { + universal_login_experience: 'classic', + identifier_first: true, + }; + const promptsDirectoryNoPartialsFile = { + [constants.PROMPTS_DIRECTORY]: { + [promptsSettingsFile]: JSON.stringify(mockPromptsSettings), + [customTextFile]: JSON.stringify({}), + }, + }; + + createDir(promptsDirectory, promptsDirectoryNoPartialsFile); + const config = { AUTH0_INPUT_FILE: promptsDirectory, }; const context = new Context(config, mockMgmtClient()); await context.loadAssetsFromLocal(); - expect(context.assets.prompts).to.deep.equal({ ...mockPromptsSettings, customText: {} }); + expect(context.assets.prompts).to.deep.equal({ + ...mockPromptsSettings, + customText: {}, + partials: {}, + }); }); it('should parse even if both files are absent', async () => { @@ -129,7 +199,7 @@ describe('#directory context prompts', () => { const context = new Context(config, mockMgmtClient()); await context.loadAssetsFromLocal(); - expect(context.assets.prompts).to.deep.equal({ customText: {} }); + expect(context.assets.prompts).to.deep.equal({ customText: {}, partials: {} }); }); }); @@ -182,6 +252,18 @@ describe('#directory context prompts', () => { }, }, }, + partials: { + login: { + 'login': { + 'form-content-start': './partials/login/form-content-start.liquid', + }, + }, + signup: { + 'signup': { + 'form-content-end': './partials/signup/form-content-end.liquid', + }, + }, + }, }; await promptsHandler.dump(context); @@ -190,6 +272,7 @@ describe('#directory context prompts', () => { expect(dumpedFiles).to.deep.equal([ path.join(promptsDirectory, customTextFile), + path.join(promptsDirectory, partialsFile), path.join(promptsDirectory, promptsSettingsFile), ]); diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 4f6021579..a9cc666ec 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -179,7 +179,7 @@ describe('#YAML context validation', () => { const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); const tenantFile = path.join(dir, 'tenant.yml'); - const context = new Context({ AUTH0_INPUT_FILE: tenantFile }, mockMgmtClient()); + const context = new Context({ AUTH0_INPUT_FILE: tenantFile , AUTH0_EXCLUDED: ['prompts'] }, mockMgmtClient()); await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); @@ -282,9 +282,6 @@ describe('#YAML context validation', () => { suspiciousIpThrottling: {}, }, logStreams: [], - prompts: { - customText: {}, - }, customDomains: [], themes: [], }); @@ -297,6 +294,7 @@ describe('#YAML context validation', () => { const config = { AUTH0_INPUT_FILE: tenantFile, AUTH0_EXCLUDED_DEFAULTS: ['emailProvider'], + AUTH0_EXCLUDED: ['prompts'], }; const context = new Context(config, mockMgmtClient()); await context.dump(); @@ -394,9 +392,6 @@ describe('#YAML context validation', () => { suspiciousIpThrottling: {}, }, logStreams: [], - prompts: { - customText: {}, - }, customDomains: [], themes: [], }); @@ -410,6 +405,7 @@ describe('#YAML context validation', () => { AUTH0_INPUT_FILE: tenantFile, INCLUDED_PROPS: { clients: ['client_secret'] }, EXCLUDED_PROPS: { clients: ['name'], emailProvider: ['credentials'] }, + AUTH0_EXCLUDED: ['prompts'], }; const context = new Context(config, mockMgmtClient()); await context.dump(); @@ -507,9 +503,6 @@ describe('#YAML context validation', () => { suspiciousIpThrottling: {}, }, logStreams: [], - prompts: { - customText: {}, - }, customDomains: [], themes: [], }); @@ -522,6 +515,7 @@ describe('#YAML context validation', () => { const config = { AUTH0_INPUT_FILE: tenantFile, INCLUDED_PROPS: { clients: ['client_secret', 'name'] }, + AUTH0_EXCLUDED: ['prompts'], EXCLUDED_PROPS: { clients: ['client_secret', 'name'] }, }; const context = new Context(config, mockMgmtClient()); @@ -594,7 +588,7 @@ describe('#YAML context validation', () => { } } ); - + await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); @@ -613,6 +607,6 @@ describe('#YAML context validation', () => { }, ], }); - sinon.restore(); + sinon.restore(); }); }); diff --git a/test/context/yaml/prompts.test.ts b/test/context/yaml/prompts.test.ts index ff815ccd7..e70f8cc2d 100644 --- a/test/context/yaml/prompts.test.ts +++ b/test/context/yaml/prompts.test.ts @@ -62,6 +62,15 @@ describe('#YAML context prompts', () => { passwordSecurityText: 'Your password must contain:' title: Create Your Account! usernamePlaceholder: Username + partials: + login: + login: + form-content-end: >- +
TEST
+ login-id: + login-id: + form-content-end: >- +
TEST
`; const yamlFile = path.join(dir, 'config.yaml'); @@ -130,6 +139,18 @@ describe('#YAML context prompts', () => { }, }, identifier_first: true, + partials: { + login: { + login: { + 'form-content-end': '
TEST
', + }, + }, + 'login-id': { + 'login-id': { + 'form-content-end': '
TEST
', + }, + }, + }, universal_login_experience: 'classic', }); }); @@ -171,6 +192,18 @@ describe('#YAML context prompts', () => { }, }, }, + partials: { + login: { + login: { + 'form-content-end': '
TEST
', + }, + }, + 'login-id': { + 'login-id': { + 'form-content-end': '
TEST
', + }, + }, + }, }; const dumped = await promptsHandler.dump(context); diff --git a/test/tools/auth0/handlers/prompts.tests.ts b/test/tools/auth0/handlers/prompts.tests.ts index 8e8ae6bf2..83e7c5c40 100644 --- a/test/tools/auth0/handlers/prompts.tests.ts +++ b/test/tools/auth0/handlers/prompts.tests.ts @@ -1,8 +1,15 @@ import { expect } from 'chai'; -import promptsHandler from '../../../../src/tools/auth0/handlers/prompts'; -import { Language } from '../../../../src/types'; import _ from 'lodash'; import { PromisePoolExecutor } from 'promise-pool-executor'; +import sinon from 'sinon'; +import axios, { AxiosResponse } from 'axios'; +import promptsHandler, { Prompts } from '../../../../src/tools/auth0/handlers/prompts'; +import { Language } from '../../../../src/types'; +import log from '../../../../src/logger'; +import PromptsHandler, { + CustomPartialsPromptTypes, CustomPromptPartials, + CustomPromptPartialsScreens +} from '../../../../lib/tools/auth0/handlers/prompts'; const mockPromptsSettings = { universal_login_experience: 'classic', @@ -11,7 +18,7 @@ const mockPromptsSettings = { describe('#prompts handler', () => { describe('#prompts process', () => { - it('should get prompts settings and prompts custom text', async () => { + it('should get prompts settings, custom texts and template partials', async () => { const supportedLanguages: Language[] = ['es', 'fr', 'en']; const englishCustomText = { @@ -48,6 +55,16 @@ describe('#prompts handler', () => { 'mfa-webauthn': {}, }; // Has no prompts configured. + const loginPartial = { + login: { + 'form-content-end': '
TEST
', + }, + }; + const signupPartial = { + signup: { + 'form-content-end': '
TEST
', + }, + }; const auth0 = { tenant: { getSettings: () => @@ -70,6 +87,7 @@ describe('#prompts handler', () => { return Promise.resolve(customTextValue); }, + // updatePartials: ({ prompt ,body } ) => Promise.resolve(body), }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -78,26 +96,48 @@ describe('#prompts handler', () => { }), }; - const handler = new promptsHandler({ client: auth0 }); + const handler = new promptsHandler( + { + client: auth0 , + } + ); + + const getCustomPartial = sinon.stub(handler, 'getCustomPartial'); + getCustomPartial.withArgs({ prompt: 'login' }).resolves(loginPartial); + getCustomPartial.withArgs({ prompt: 'login-id' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'login-password' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'signup-password' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'signup-id' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'signup' }).resolves(signupPartial); + const data = await handler.getType(); expect(data).to.deep.equal({ ...mockPromptsSettings, customText: { en: { 'signup-password': englishCustomText['signup-password'], - login: englishCustomText['login'], + login: englishCustomText.login, }, fr: { - login: frenchCustomText['login'], + login: frenchCustomText.login, + }, + // does not have spanish custom text because all responses returned empty objects + }, + partials: { + login:{ + login:loginPartial.login, }, - //does not have spanish custom text because all responses returned empty objects + signup: { + signup:signupPartial.signup, + } }, }); }); - it('should update prompts settings but not custom text settings if not set', async () => { + it('should update prompts settings but not custom text/partials settings if not set', async () => { let didCallUpdatePromptsSettings = false; let didCallUpdateCustomText = false; + let didCallUpdatePartials = false; const auth0 = { tenant: { @@ -117,19 +157,30 @@ describe('#prompts handler', () => { }, }; - const handler = new promptsHandler({ client: auth0 }); - const stageFn = Object.getPrototypeOf(handler).processChanges; + const handler = new promptsHandler( + { + client: auth0 , + } + ); + sinon.stub(handler, 'updateCustomPartials').callsFake(() => { + didCallUpdatePartials = true; + return Promise.resolve({}); + }); + const stageFn = handler.processChanges.bind(handler); const customText = undefined; - await stageFn.apply(handler, [{ prompts: { ...mockPromptsSettings, customText } }]); + await stageFn.apply(handler, [{ prompts: { ...mockPromptsSettings, customText }, partials: undefined }]); expect(didCallUpdatePromptsSettings).to.equal(true); expect(didCallUpdateCustomText).to.equal(false); + expect(didCallUpdatePartials).to.equal(false); }); - it('should update prompts settings and custom text settings when both are set', async () => { + it('should update prompts settings and custom text/partials settings when set', async () => { let didCallUpdatePromptsSettings = false; let didCallUpdateCustomText = false; + let didCallUpdatePartials = false; let numberOfUpdateCustomTextCalls = 0; + let numberOfUpdatePartialsCalls = 0; const customTextToSet = { en: { @@ -149,6 +200,24 @@ describe('#prompts handler', () => { }, }; + const partialsToSet: Prompts['partials'] = { + login: { + login: { + 'form-content-start': '
TEST
', + }, + }, + 'signup-id': { + 'signup-id': { + 'form-content-start': '
TEST
', + }, + }, + 'signup-password': { + 'signup-password': { + 'form-content-start': '
TEST
', + }, + }, + }; + const auth0 = { prompts: { updateCustomTextByLanguage: () => { @@ -169,19 +238,28 @@ describe('#prompts handler', () => { }), }; - const handler = new promptsHandler({ client: auth0 }); - const stageFn = Object.getPrototypeOf(handler).processChanges; + const handler = new promptsHandler({ client: auth0, + }); + + sinon.stub(handler, 'updateCustomPartials').callsFake(() => { + didCallUpdatePartials = true; + numberOfUpdatePartialsCalls++; + return Promise.resolve({}); + }); + const stageFn = Object.getPrototypeOf(handler).processChanges; await stageFn.apply(handler, [ - { prompts: { ...mockPromptsSettings, customText: customTextToSet } }, + { prompts: { ...mockPromptsSettings, customText: customTextToSet, partials: partialsToSet } }, ]); expect(didCallUpdatePromptsSettings).to.equal(true); expect(didCallUpdateCustomText).to.equal(true); + expect(didCallUpdatePartials).to.equal(true); expect(numberOfUpdateCustomTextCalls).to.equal(3); + expect(numberOfUpdatePartialsCalls).to.equal(3); }); - it('should not fail if tenant languages undefined', async () => { + it('should not fail if tenant languages or partials are undefined', async () => { const auth0 = { tenant: { getSettings: () => @@ -200,11 +278,200 @@ describe('#prompts handler', () => { }; const handler = new promptsHandler({ client: auth0 }); + const getCustomPartial = sinon.stub(handler, 'getCustomPartial'); + getCustomPartial.withArgs({ prompt: 'login' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'login-id' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'login-password' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'signup-password' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'signup-id' }).resolves({}); + getCustomPartial.withArgs({ prompt: 'signup' }).resolves({}); + const data = await handler.getType(); expect(data).to.deep.equal({ ...mockPromptsSettings, customText: {}, // Custom text empty + partials: {}, // Partials empty + }); + }); + + it('should check if getPartialsEndpoint and putPartialsEndpoint give correct domain', () => { + const handler = new promptsHandler( + { + config: function() { return 'test-host.auth0.com'; } , } as any); + + expect(handler.getPartialsEndpoint('login' )).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); + expect(handler.putPartialsEndpoint('login')).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); + }); + }); + describe('withErrorHandling', () => { + let handler: any; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + const auth0 = { + tokenProvider: { + getAccessToken: async function () { + return 'test-access-token'; + }, + }, + }; + handler = new promptsHandler({ client: auth0 }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the result of the callback', async () => { + const callback = sandbox.stub().resolves('success'); + const result = await handler.withErrorHandling(callback); + expect(result).to.equal('success'); + }); + + it('should handle 403 error and set IsFeatureSupported to false', async () => { + const error = { + response: { + data: { + statusCode: 403, + }, + }, + }; + const callback = sandbox.stub().rejects(error); + const logWarn = sandbox.stub(log, 'warn'); + + const result = await handler.withErrorHandling(callback); + expect(result).to.deep.equal({ data: null }); + expect(handler.IsFeatureSupported).to.be.false; + expect(logWarn.calledWith('Partial Prompts feature is not supported for the tenant')).to.be.true; + }); + + it('should handle 400 error with specific message and set IsFeatureSupported to false', async () => { + const error = { + response: { + data: { + statusCode: 400, + message: 'This feature requires at least one custom domain to be configured for the tenant.', + }, + }, + }; + const callback = sandbox.stub().rejects(error); + const logWarn = sandbox.stub(log, 'warn'); + + const result = await handler.withErrorHandling(callback); + expect(result).to.deep.equal({ data: null }); + expect(handler.IsFeatureSupported).to.be.false; + expect(logWarn.calledWith('Partial Prompts feature requires at least one custom domain to be configured for the tenant')).to.be.true; + }); + + it('should rethrow other errors', async () => { + const error = new Error('some other error'); + const callback = sandbox.stub().rejects(error); + + try { + await handler.withErrorHandling(callback); + throw new Error('Expected method to throw.'); + } catch (err) { + expect(err).to.equal(error); + } + }); + + it('should make an HTTP request with the correct headers', async () => { + const url = 'https://example.com'; + const body = { key: 'value' }; + const axiosStub = sandbox.stub(axios, 'put').resolves({ data: 'response' } as AxiosResponse); + + const result = await handler.partialHttpRequest('put', [url, body]); + expect(axiosStub.calledOnce).to.be.true; + const { args } = axiosStub.getCall(0); + expect(args[0]).to.equal(url); + expect(args[1]).to.deep.equal(body); + expect(args[2]).to.deep.include({ + headers: { + Accept: 'application/json', + Authorization: 'Bearer test-access-token', + }, }); + expect(result).to.deep.equal({ data: 'response' }); + }); + + it('should handle errors correctly', async () => { + const error = new Error('Request failed'); + sandbox.stub(axios, 'put').rejects(error); + + try { + await handler.partialHttpRequest('put', ['https://example.com', {}]); + throw new Error('Expected method to throw.'); + } catch (err) { + expect(err).to.equal(error); + } + }); + + it('should not make a request if the feature is not supported', async () => { + handler.IsFeatureSupported = false; + const putStub = sandbox.stub(handler, 'partialHttpRequest'); + + await handler.updateCustomPartials({ prompt: 'login', body: {} as CustomPromptPartialsScreens }); + + expect(putStub.called).to.be.false; + }); + + it('should make a request if the feature is supported', async () => { + handler.IsFeatureSupported = true; + const url = 'https://example.com'; + const body = { key: 'value' }; + sandbox.stub(handler, 'putPartialsEndpoint').returns(url); + const putStub = sandbox.stub(handler, 'partialHttpRequest').resolves(); + + await handler.updateCustomPartials({ prompt: 'login', body }); + + expect(putStub.calledOnce).to.be.true; + const { args } = putStub.getCall(0); + expect(args[0]).to.equal('put'); + expect(args[1]).to.deep.equal([url, body]); + }); + + it('should return empty object if feature is not supported', async () => { + handler.IsFeatureSupported = false; + + const result = await handler.getCustomPartial({ prompt: 'login' as CustomPartialsPromptTypes }); + expect(result).to.deep.equal({}); + }); + + it('should return custom partial data if feature is supported', async () => { + handler.IsFeatureSupported = true; + + const mockResponse = { + data: { + 'form-content-end': '
TEST
' + } + } as unknown as AxiosResponse; + + const url = 'https://test-host.auth0.com/api/v2/prompts/login/partials'; + sandbox.stub(handler, 'getPartialsEndpoint').returns(url); + sandbox.stub(handler, 'partialHttpRequest').resolves(mockResponse); + + const result = await handler.getCustomPartial({ prompt: 'login' as CustomPartialsPromptTypes }); + + expect(result).to.deep.equal(mockResponse.data); + expect(handler.getPartialsEndpoint.calledOnceWith('login')).to.be.true; + expect(handler.partialHttpRequest.calledOnceWith('get', [url])).to.be.true; + }); + + it('should handle errors correctly', async () => { + handler.IsFeatureSupported = true; + + const url = 'https://test-host.auth0.com/api/v2/prompts/login/partials'; + sandbox.stub(handler, 'getPartialsEndpoint').returns(url); + const error = new Error('Request failed'); + sandbox.stub(handler, 'partialHttpRequest').rejects(error); + + try { + await handler.getCustomPartial({ prompt: 'login' as CustomPartialsPromptTypes }); + throw new Error('Expected method to throw.'); + } catch (err) { + expect(err).to.equal(error); + } }); }); }); diff --git a/test/utils.js b/test/utils.js index 4a61197a3..add60e081 100644 --- a/test/utils.js +++ b/test/utils.js @@ -144,7 +144,8 @@ export function createDir(repoDir, files) { const configDir = path.resolve(repoDir, type); cleanThenMkdir(configDir); Object.entries(files[type]).forEach(([name, content]) => { - fs.writeFileSync(path.join(configDir, name), content); + const filePath = path.join(configDir, name); + fs.writeFileSync(filePath, content); }); }); } From 8ab144d9bd710ea3f367540cf48703dfddae477d Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 00:09:49 +0530 Subject: [PATCH 02/20] Fixed Linting --- src/tools/auth0/handlers/prompts.ts | 4 ++-- test/context/directory/prompts.test.ts | 2 +- test/context/yaml/context.test.js | 2 +- test/tools/auth0/handlers/prompts.tests.ts | 20 +++++++++++--------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 71d9df047..8f571cf8f 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -350,7 +350,7 @@ export default class PromptsHandler extends DefaultHandler { return { data: null }; } - if(errorData?.statusCode === 400 && errorData?.message === 'This feature requires at least one custom domain to be configured for the tenant.') { + if (errorData?.statusCode === 400 && errorData?.message === 'This feature requires at least one custom domain to be configured for the tenant.') { log.warn('Partial Prompts feature requires at least one custom domain to be configured for the tenant'); this.IsFeatureSupported = false; return { data: null }; @@ -471,7 +471,7 @@ export default class PromptsHandler extends DefaultHandler { prompt, }; }), - generator: ({ prompt, body }) => this.updateCustomPartials({ prompt ,body } ), + generator: ({ prompt, body }) => this.updateCustomPartials({ prompt, body }), }) .promise(); } diff --git a/test/context/directory/prompts.test.ts b/test/context/directory/prompts.test.ts index 5aa6047cf..8c4940944 100644 --- a/test/context/directory/prompts.test.ts +++ b/test/context/directory/prompts.test.ts @@ -92,7 +92,7 @@ describe('#directory context prompts', () => { identifier_first: true, partials: { login: { - login:{ + login: { 'form-content-start': '
TEST
', } }, diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index a9cc666ec..26563529f 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -179,7 +179,7 @@ describe('#YAML context validation', () => { const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); const tenantFile = path.join(dir, 'tenant.yml'); - const context = new Context({ AUTH0_INPUT_FILE: tenantFile , AUTH0_EXCLUDED: ['prompts'] }, mockMgmtClient()); + const context = new Context({ AUTH0_INPUT_FILE: tenantFile, AUTH0_EXCLUDED: ['prompts'] }, mockMgmtClient()); await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); diff --git a/test/tools/auth0/handlers/prompts.tests.ts b/test/tools/auth0/handlers/prompts.tests.ts index 83e7c5c40..8c0df6e0c 100644 --- a/test/tools/auth0/handlers/prompts.tests.ts +++ b/test/tools/auth0/handlers/prompts.tests.ts @@ -55,7 +55,7 @@ describe('#prompts handler', () => { 'mfa-webauthn': {}, }; // Has no prompts configured. - const loginPartial = { + const loginPartial = { login: { 'form-content-end': '
TEST
', }, @@ -98,7 +98,7 @@ describe('#prompts handler', () => { const handler = new promptsHandler( { - client: auth0 , + client: auth0, } ); @@ -124,11 +124,11 @@ describe('#prompts handler', () => { // does not have spanish custom text because all responses returned empty objects }, partials: { - login:{ - login:loginPartial.login, + login: { + login: loginPartial.login, }, signup: { - signup:signupPartial.signup, + signup: signupPartial.signup, } }, }); @@ -159,7 +159,7 @@ describe('#prompts handler', () => { const handler = new promptsHandler( { - client: auth0 , + client: auth0, } ); sinon.stub(handler, 'updateCustomPartials').callsFake(() => { @@ -238,7 +238,8 @@ describe('#prompts handler', () => { }), }; - const handler = new promptsHandler({ client: auth0, + const handler = new promptsHandler({ + client: auth0, }); sinon.stub(handler, 'updateCustomPartials').callsFake(() => { @@ -297,9 +298,10 @@ describe('#prompts handler', () => { it('should check if getPartialsEndpoint and putPartialsEndpoint give correct domain', () => { const handler = new promptsHandler( { - config: function() { return 'test-host.auth0.com'; } , } as any); + config: function () { return 'test-host.auth0.com'; }, + } as any); - expect(handler.getPartialsEndpoint('login' )).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); + expect(handler.getPartialsEndpoint('login')).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); expect(handler.putPartialsEndpoint('login')).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); }); }); From 7a1364f56fa754a8d00b24c57efc046d275d0b9e Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 15:09:18 +0530 Subject: [PATCH 03/20] Fixed context.test.js --- test/context/yaml/context.test.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 26563529f..6d203f27a 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -3,12 +3,20 @@ import fs from 'fs-extra'; import jsYaml from 'js-yaml'; import { expect } from 'chai'; import sinon from 'sinon'; - +import handlers from '../../../src/tools/auth0/handlers'; import Context from '../../../src/context/yaml'; import { cleanThenMkdir, testDataDir, mockMgmtClient } from '../../utils'; import ScimHandler from '../../../src/tools/auth0/handlers/scimHandler'; describe('#YAML context validation', () => { + beforeEach(() => { + sinon.stub(handlers.prompts.default.prototype, 'getCustomPartial').resolves({}); + }); + + afterEach(() => { + sinon.restore(); + }); + it('should do nothing on empty yaml', async () => { /* Create empty directory */ const dir = path.resolve(testDataDir, 'yaml', 'empty'); @@ -179,7 +187,7 @@ describe('#YAML context validation', () => { const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); const tenantFile = path.join(dir, 'tenant.yml'); - const context = new Context({ AUTH0_INPUT_FILE: tenantFile, AUTH0_EXCLUDED: ['prompts'] }, mockMgmtClient()); + const context = new Context({ AUTH0_INPUT_FILE: tenantFile }, mockMgmtClient()); await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); @@ -281,6 +289,10 @@ describe('#YAML context validation', () => { bruteForceProtection: {}, suspiciousIpThrottling: {}, }, + prompts: { + customText: {}, + partials: {} + }, logStreams: [], customDomains: [], themes: [], @@ -294,7 +306,6 @@ describe('#YAML context validation', () => { const config = { AUTH0_INPUT_FILE: tenantFile, AUTH0_EXCLUDED_DEFAULTS: ['emailProvider'], - AUTH0_EXCLUDED: ['prompts'], }; const context = new Context(config, mockMgmtClient()); await context.dump(); @@ -391,6 +402,10 @@ describe('#YAML context validation', () => { bruteForceProtection: {}, suspiciousIpThrottling: {}, }, + prompts: { + customText: {}, + partials: {} + }, logStreams: [], customDomains: [], themes: [], @@ -405,7 +420,6 @@ describe('#YAML context validation', () => { AUTH0_INPUT_FILE: tenantFile, INCLUDED_PROPS: { clients: ['client_secret'] }, EXCLUDED_PROPS: { clients: ['name'], emailProvider: ['credentials'] }, - AUTH0_EXCLUDED: ['prompts'], }; const context = new Context(config, mockMgmtClient()); await context.dump(); @@ -502,6 +516,10 @@ describe('#YAML context validation', () => { bruteForceProtection: {}, suspiciousIpThrottling: {}, }, + prompts: { + customText: {}, + partials: {} + }, logStreams: [], customDomains: [], themes: [], From 606073c80f0b887381c22a41825365009b639e8f Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 16:59:07 +0530 Subject: [PATCH 04/20] Add E2e Test --- ...should-dump-without-throwing-an-error.json | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/test/e2e/recordings/should-dump-without-throwing-an-error.json b/test/e2e/recordings/should-dump-without-throwing-an-error.json index f67efad27..30682a7d5 100644 --- a/test/e2e/recordings/should-dump-without-throwing-an-error.json +++ b/test/e2e/recordings/should-dump-without-throwing-an-error.json @@ -1680,6 +1680,66 @@ "rawHeaders": [], "responseIsBinary": false }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, { "scope": "https://deploy-cli-dev.eu.auth0.com:443", "method": "GET", @@ -2605,4 +2665,4 @@ "rawHeaders": [], "responseIsBinary": false } -] \ No newline at end of file +] From 714207b1123b99c73f6dccae6d41f2a1fb315d3b Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 17:05:51 +0530 Subject: [PATCH 05/20] Fix E2e Test --- .../testdata/should-deploy-without-throwing-an-error/tenant.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/testdata/should-deploy-without-throwing-an-error/tenant.yaml b/test/e2e/testdata/should-deploy-without-throwing-an-error/tenant.yaml index c5dac8f38..1b0aa2fb6 100644 --- a/test/e2e/testdata/should-deploy-without-throwing-an-error/tenant.yaml +++ b/test/e2e/testdata/should-deploy-without-throwing-an-error/tenant.yaml @@ -124,6 +124,7 @@ branding: templates: [] prompts: customText: {} + partials: {} universal_login_experience: new migrations: {} actions: [] From 35b06a75d515b4551965c89835a401321425cd77 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 17:10:46 +0530 Subject: [PATCH 06/20] Fix E2e Test --- ...ources-if-AUTH0_ALLOW_DELETE-is-false.json | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/test/e2e/recordings/should-deploy-without-deleting-resources-if-AUTH0_ALLOW_DELETE-is-false.json b/test/e2e/recordings/should-deploy-without-deleting-resources-if-AUTH0_ALLOW_DELETE-is-false.json index 3bd2b0840..79b5a8023 100644 --- a/test/e2e/recordings/should-deploy-without-deleting-resources-if-AUTH0_ALLOW_DELETE-is-false.json +++ b/test/e2e/recordings/should-deploy-without-deleting-resources-if-AUTH0_ALLOW_DELETE-is-false.json @@ -12748,6 +12748,66 @@ "rawHeaders": [], "responseIsBinary": false }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, { "scope": "https://deploy-cli-dev.eu.auth0.com:443", "method": "GET", @@ -13599,4 +13659,4 @@ "rawHeaders": [], "responseIsBinary": false } -] \ No newline at end of file +] From 396940900a0b16ff27db27e67ebaac383f14a3bb Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 25 Jul 2024 22:36:24 +0530 Subject: [PATCH 07/20] Fix E2E Tests --- ...sources-if-AUTH0_ALLOW_DELETE-is-true.json | 62 ++++++++++++++++++- ...-and-deploy-without-throwing-an-error.json | 62 ++++++++++++++++++- .../lots-of-configuration/tenant.yaml | 1 + 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/test/e2e/recordings/should-deploy-while-deleting-resources-if-AUTH0_ALLOW_DELETE-is-true.json b/test/e2e/recordings/should-deploy-while-deleting-resources-if-AUTH0_ALLOW_DELETE-is-true.json index 5c0660552..82a4a8d93 100644 --- a/test/e2e/recordings/should-deploy-while-deleting-resources-if-AUTH0_ALLOW_DELETE-is-true.json +++ b/test/e2e/recordings/should-deploy-while-deleting-resources-if-AUTH0_ALLOW_DELETE-is-true.json @@ -11249,6 +11249,66 @@ "rawHeaders": [], "responseIsBinary": false }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, { "scope": "https://deploy-cli-dev.eu.auth0.com:443", "method": "GET", @@ -11937,4 +11997,4 @@ "rawHeaders": [], "responseIsBinary": false } -] \ No newline at end of file +] diff --git a/test/e2e/recordings/should-dump-and-deploy-without-throwing-an-error.json b/test/e2e/recordings/should-dump-and-deploy-without-throwing-an-error.json index 7ba389f22..724c9a67f 100644 --- a/test/e2e/recordings/should-dump-and-deploy-without-throwing-an-error.json +++ b/test/e2e/recordings/should-dump-and-deploy-without-throwing-an-error.json @@ -1722,6 +1722,66 @@ "rawHeaders": [], "responseIsBinary": false }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/login-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-id/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, + { + "scope": "https://deploy-cli-dev.eu.auth0.com:443", + "method": "GET", + "path": "/api/v2/prompts/signup-password/partials", + "body": "", + "status": 200, + "response": {}, + "rawHeaders": [], + "responseIsBinary": false + }, { "scope": "https://deploy-cli-dev.eu.auth0.com:443", "method": "GET", @@ -5064,4 +5124,4 @@ "rawHeaders": [], "responseIsBinary": false } -] \ No newline at end of file +] diff --git a/test/e2e/testdata/lots-of-configuration/tenant.yaml b/test/e2e/testdata/lots-of-configuration/tenant.yaml index a943dc264..3e7b3a63b 100644 --- a/test/e2e/testdata/lots-of-configuration/tenant.yaml +++ b/test/e2e/testdata/lots-of-configuration/tenant.yaml @@ -657,6 +657,7 @@ roles: description: Readz Only permissions: [] prompts: + partials: {} universal_login_experience: new identifier_first: true migrations: {} From bee624442e4ff9f2036dbfcf739efcc77adbd782 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Fri, 26 Jul 2024 02:37:49 +0530 Subject: [PATCH 08/20] Update structure --- src/context/directory/handlers/prompts.ts | 62 +++++++++++------------ src/tools/auth0/handlers/prompts.ts | 8 ++- test/context/directory/prompts.test.ts | 37 ++++++++++---- test/utils.js | 28 +++++++++- 4 files changed, 90 insertions(+), 45 deletions(-) diff --git a/src/context/directory/handlers/prompts.ts b/src/context/directory/handlers/prompts.ts index 5ba88946d..f741c8606 100644 --- a/src/context/directory/handlers/prompts.ts +++ b/src/context/directory/handlers/prompts.ts @@ -8,11 +8,14 @@ import { ParsedAsset } from '../../../types'; import { AllPromptsByLanguage, CustomPartialsConfig, + CustomPartialsInsertionPoints, CustomPartialsPromptTypes, + CustomPartialsScreenTypes, + CustomPromptPartialsScreens, Prompts, - PromptSettings + PromptSettings, + ScreenConfig } from '../../../tools/auth0/handlers/prompts'; -import log from '../../../logger'; type ParsedPrompts = ParsedAsset<'prompts', Prompts>; @@ -22,9 +25,7 @@ const getPromptsSettingsFile = (promptsDirectory: string) => path.join(promptsDi const getCustomTextFile = (promptsDirectory: string) => path.join(promptsDirectory, 'custom-text.json'); -const getPartialsFile = (promptsDirectory: string) => { - return path.join(promptsDirectory, 'partials.json'); -}; +const getPartialsFile = (promptsDirectory: string) => path.join(promptsDirectory, 'partials.json'); function parse(context: DirectoryContext): ParsedPrompts { const promptsDirectory = getPromptsDirectory(context.filePath); @@ -56,25 +57,18 @@ function parse(context: DirectoryContext): ParsedPrompts { disableKeywordReplacement: context.disableKeywordReplacement, }) as CustomPartialsConfig; - return Object.entries(partialsFileContent).reduce((acc, [promptName, items]) => { - acc[promptName] = items.reduce((screenAcc, { name, template }) => { - if (!screenAcc[promptName]) { - screenAcc[promptName] = {}; - } - - // Read template content from the file - const templateFilePath = path.join(promptsDirectory, template); - if (isFile(templateFilePath)) { - const templateContent = readFileSync(templateFilePath, 'utf8'); - if (templateContent.trim()) { - screenAcc[promptName][name] = templateContent; - } - } - + return Object.entries(partialsFileContent).reduce((acc, [promptName, screensArray]) => { + const screensObject = screensArray[0] as Record; + acc[promptName as CustomPartialsPromptTypes] = Object.entries(screensObject).reduce((screenAcc, [screenName, items]) => { + screenAcc[screenName as CustomPartialsScreenTypes] = items.reduce((insertionAcc, { name, template }) => { + const templateFilePath = path.join(promptsDirectory, template); + insertionAcc[name] = isFile(templateFilePath) ? readFileSync(templateFilePath, 'utf8').trim() : ''; + return insertionAcc; + }, {} as Record); return screenAcc; - }, {} as Record>); + }, {} as Record>); return acc; - }, {} as Record>>); + }, {} as Record>>); })(); return { @@ -107,17 +101,21 @@ async function dump(context: DirectoryContext): Promise { if (!partials) return; const partialsFile = getPartialsFile(promptsDirectory); - // Transform the partials data back to CustomPartialsConfig format const transformedPartials = Object.entries(partials).reduce((acc, [promptName, screens]) => { - acc[promptName] = Object.entries(screens).map(([, insertionPoints]) => Object.entries(insertionPoints).map(([insertionPoint, template]) => { - const templateFilePath = path.join(promptsDirectory, 'partials', promptName, `${insertionPoint}.liquid`); - ensureDirSync(path.dirname(templateFilePath)); - writeFileSync(templateFilePath, template, 'utf8'); - return { - name: insertionPoint, - template: path.relative(promptsDirectory, templateFilePath) // Path relative to `promptsDirectory` - }; - })).flat(); // Flatten the nested arrays into a single array + acc[promptName as CustomPartialsPromptTypes] = [ + Object.entries(screens as CustomPromptPartialsScreens).reduce((screenAcc, [screenName, insertionPoints]) => { + screenAcc[screenName as CustomPartialsScreenTypes] = Object.entries(insertionPoints as Partial>).map(([insertionPoint, template]) => { + const templateFilePath = path.join(promptsDirectory, 'partials', promptName, screenName, `${insertionPoint}.liquid`); + ensureDirSync(path.dirname(templateFilePath)); + writeFileSync(templateFilePath, template, 'utf8'); + return { + name: insertionPoint, + template: path.relative(promptsDirectory, templateFilePath) // Path relative to `promptsDirectory` + }; + }); + return screenAcc; + }, {} as Record) + ]; return acc; }, {} as CustomPartialsConfig); diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 8f571cf8f..5c7311dbb 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -138,11 +138,15 @@ export type CustomPromptPartials = Partial<{ [prompt in CustomPartialsPromptTypes]: CustomPromptPartialsScreens; }>; +export interface ScreenConfig { + name: string; + template: string; +} + export type CustomPartialsConfig = { [prompt in CustomPartialsPromptTypes]: [ { - name: string; - template: string; + [screen in CustomPartialsScreenTypes]: ScreenConfig[]; } ]; }; diff --git a/test/context/directory/prompts.test.ts b/test/context/directory/prompts.test.ts index 8c4940944..6a5e7206e 100644 --- a/test/context/directory/prompts.test.ts +++ b/test/context/directory/prompts.test.ts @@ -5,7 +5,7 @@ import { constants } from '../../../src/tools'; import Context from '../../../src/context/directory'; import promptsHandler from '../../../src/context/directory/handlers/prompts'; import { getFiles, loadJSON } from '../../../src/utils'; -import { cleanThenMkdir, testDataDir, createDir, mockMgmtClient } from '../../utils'; +import { cleanThenMkdir, testDataDir, createDir, mockMgmtClient, createDirWithNestedDir } from '../../utils'; const dir = path.join(testDataDir, 'directory', 'promptsDump'); const promptsDirectory = path.join(dir, constants.PROMPTS_DIRECTORY); @@ -49,15 +49,23 @@ describe('#directory context prompts', () => { [partialsFile]: JSON.stringify({ login: [ { - name: 'form-content-start', - template: 'partials/login/form-content-start.liquid', - }, + login: [ + { + name: 'form-content-start', + template: 'partials/login/login/form-content-start.liquid', + }, + ] + } ], signup: [ { - name: 'form-content-end', - template: 'partials/signup/form-content-end.liquid', - }, + signup: [ + { + name: 'form-content-end', + template: 'partials/signup/signup/form-content-end.liquid', + }, + ] + } ], }), }, @@ -71,12 +79,21 @@ describe('#directory context prompts', () => { constants.PROMPTS_DIRECTORY, constants.PARTIALS_DIRECTORY ); + const partialsFiles = { - login: { 'form-content-start.liquid': '
TEST
' }, - signup: { 'form-content-end.liquid': '
TEST AGAIN
' }, + login: { + login: { + 'form-content-start.liquid': '
TEST
', + } + }, + signup: { + signup: { + 'form-content-end.liquid': '
TEST AGAIN
', + }, + } }; - createDir(partialsDir, partialsFiles); + createDirWithNestedDir(partialsDir, partialsFiles); const config = { AUTH0_INPUT_FILE: repoDir, diff --git a/test/utils.js b/test/utils.js index add60e081..44267bf72 100644 --- a/test/utils.js +++ b/test/utils.js @@ -124,7 +124,7 @@ export function mockMgmtClient() { new Promise((res) => { res({}); }), - getSettings: () => {}, + getSettings: () => { }, }, customDomains: { getAll: () => [] }, }; @@ -149,3 +149,29 @@ export function createDir(repoDir, files) { }); }); } + +export function createDirWithNestedDir(repoDir, files) { + Object.keys(files).forEach((type) => { + const typeDir = path.resolve(repoDir, type); + cleanThenMkdir(typeDir); + + Object.entries(files[type]).forEach(([subtype, content]) => { + const subtypeDir = path.join(typeDir, subtype); + + if (typeof content === 'string') { + fs.writeFileSync(subtypeDir, content); + } else if (typeof content === 'object') { + cleanThenMkdir(subtypeDir); + Object.entries(content).forEach(([fileName, fileContent]) => { + const filePath = path.join(subtypeDir, fileName); + if (typeof fileContent !== 'string') { + throw new TypeError(`Expected content to be a string, but received ${typeof fileContent}`); + } + fs.writeFileSync(filePath, fileContent); + }); + } else { + throw new TypeError(`Expected content to be a string or object, but received ${typeof content}`); + } + }); + }); +} From 32e71dac54181556f142ad61d1c9bef6fa5d9cd8 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Fri, 26 Jul 2024 11:14:58 +0530 Subject: [PATCH 09/20] Updated Testcase for checking the file. --- test/context/directory/prompts.test.ts | 36 +++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/test/context/directory/prompts.test.ts b/test/context/directory/prompts.test.ts index 6a5e7206e..9f242ef9d 100644 --- a/test/context/directory/prompts.test.ts +++ b/test/context/directory/prompts.test.ts @@ -271,15 +271,15 @@ describe('#directory context prompts', () => { }, partials: { login: { - 'login': { - 'form-content-start': './partials/login/form-content-start.liquid', - }, + login: { + 'form-content-start': '
TEST
', + } }, signup: { - 'signup': { - 'form-content-end': './partials/signup/form-content-end.liquid', + signup: { + 'form-content-end': '
TEST AGAIN
', }, - }, + } }, }; @@ -296,6 +296,30 @@ describe('#directory context prompts', () => { expect(loadJSON(path.join(promptsDirectory, customTextFile), {})).to.deep.equal( context.assets.prompts.customText ); + expect(loadJSON(path.join(promptsDirectory, partialsFile), {})).to.deep.equal( + { + login: [ + { + login: [ + { + name: 'form-content-start', + template: 'partials/login/login/form-content-start.liquid', + }, + ] + } + ], + signup: [ + { + signup: [ + { + name: 'form-content-end', + template: 'partials/signup/signup/form-content-end.liquid', + }, + ] + } + ], + } + ); expect(loadJSON(path.join(promptsDirectory, promptsSettingsFile), {})).to.deep.equal({ universal_login_experience: context.assets.prompts.universal_login_experience, identifier_first: context.assets.prompts.identifier_first, From b701d3e6aa6c46dd7e6b1697abd0f6ad008ec54e Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Mon, 29 Jul 2024 15:45:41 +0530 Subject: [PATCH 10/20] Update - Used _getRestClient for making requests. --- src/tools/auth0/handlers/prompts.ts | 54 ++++----- src/types.ts | 3 +- test/tools/auth0/handlers/prompts.tests.ts | 129 ++++++++++----------- 3 files changed, 86 insertions(+), 100 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 5c7311dbb..51d4f0128 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -1,7 +1,6 @@ import { isEmpty } from 'lodash'; -import axios, { AxiosResponse } from 'axios'; import DefaultHandler from './default'; -import { Assets, Language, languages } from '../../../types'; +import { Asset, Assets, Language, languages } from '../../../types'; import log from '../../../logger'; const promptTypes = [ @@ -102,7 +101,7 @@ const customPartialsPromptTypes = [ 'signup', 'signup-id', 'signup-password', -] as const; +]; export type CustomPartialsPromptTypes = typeof customPartialsPromptTypes[number]; @@ -242,16 +241,6 @@ export default class PromptsHandler extends DefaultHandler { /** * Returns formatted endpoint url. */ - private getPartialsEndpoint(promptType: CustomPartialsPromptTypes) { - return `https://${this.config('AUTH0_DOMAIN')}/api/v2/prompts/${promptType}/partials`; - } - - /** - * Returns formatted endpoint url. - */ - private putPartialsEndpoint(promptType: CustomPartialsPromptTypes) { - return `https://${this.config('AUTH0_DOMAIN')}/api/v2/prompts/${promptType}/partials`; - } constructor(options: DefaultHandler) { super({ @@ -325,17 +314,12 @@ export default class PromptsHandler extends DefaultHandler { }, {})); } - private async partialHttpRequest(method: string, options: [string, ...Record[]]): Promise { - return this.withErrorHandling(async () => { - // @ts-ignore - const accessToken = await this.client.tokenProvider?.getAccessToken(); - const headers = { - 'Accept': 'application/json', - 'Authorization': `Bearer ${accessToken}` - }; - options = [...options, { headers }]; - return axios[method](...options); - },); + private promptClient = this.client.prompts._getRestClient('/prompts/:prompt/partials'); + + private async partialHttpRequest(method: string, options: [{ prompt: string }, ...Record[]]): Promise { + return this.withErrorHandling(async () => + this.promptClient[method](...options) + ); } /** @@ -347,27 +331,30 @@ export default class PromptsHandler extends DefaultHandler { } catch (error) { // Extract error data - const errorData = error?.response?.data; - if (errorData?.statusCode === 403) { + if (error && error?.statusCode === 403) { log.warn('Partial Prompts feature is not supported for the tenant'); this.IsFeatureSupported = false; - return { data: null }; + return null; } - if (errorData?.statusCode === 400 && errorData?.message === 'This feature requires at least one custom domain to be configured for the tenant.') { + if (error && error?.statusCode === 400 && error.message?.includes('feature requires at least one custom domain')) { log.warn('Partial Prompts feature requires at least one custom domain to be configured for the tenant'); this.IsFeatureSupported = false; - return { data: null }; + return null; } + + if (error && error.statusCode === 429) { + log.error(`The global rate limit has been exceeded, resulting in a ${ error.statusCode } error. ${ error.message }. Although this is an error, it is not blocking the pipeline.`); + return null; + } + throw error; } } async getCustomPartial({ prompt }: { prompt: CustomPartialsPromptTypes }): Promise { if (!this.IsFeatureSupported) return {}; - const url = this.getPartialsEndpoint(prompt); // Implement this method to return the correct endpoint URL - const response = await this.partialHttpRequest('get', [url]); // Implement this method for making HTTP requests - return response.data; + return this.partialHttpRequest('get', [{ prompt:prompt }]); // Implement this method for making HTTP requests } async getCustomPromptsPartials(): Promise { @@ -457,8 +444,7 @@ export default class PromptsHandler extends DefaultHandler { async updateCustomPartials({ prompt, body }: { prompt: CustomPartialsPromptTypes; body: CustomPromptPartialsScreens }): Promise { if (!this.IsFeatureSupported) return; - const url = this.putPartialsEndpoint(prompt); // Implement this method to return the correct endpoint URL - await this.partialHttpRequest('put', [url, body]); // Implement this method for making HTTP requests + await this.partialHttpRequest('put', [ { prompt:prompt },body]); // Implement this method for making HTTP requests } async updateCustomPromptsPartials(partials: Prompts['partials']): Promise { diff --git a/src/types.ts b/src/types.ts index 8fe333ee1..7a2338c6f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { PromisePoolExecutor } from 'promise-pool-executor'; import { Action } from './tools/auth0/handlers/actions'; import { PromptTypes, @@ -13,7 +14,6 @@ import { LogStream } from './tools/auth0/handlers/logStreams'; import { Client } from './tools/auth0/handlers/clients'; import { ClientGrant } from './tools/auth0/handlers/clientGrants'; import { ResourceServer } from './tools/auth0/handlers/resourceServers'; -import { PromisePoolExecutor } from 'promise-pool-executor'; type SharedPaginationParams = { checkpoint?: boolean; @@ -150,6 +150,7 @@ export type BaseAuth0APIClient = { }; }; prompts: { + _getRestClient(endpoint: string): Promise; updateCustomTextByLanguage: (arg0: { prompt: PromptTypes; language: Language; diff --git a/test/tools/auth0/handlers/prompts.tests.ts b/test/tools/auth0/handlers/prompts.tests.ts index 8c0df6e0c..9b290285e 100644 --- a/test/tools/auth0/handlers/prompts.tests.ts +++ b/test/tools/auth0/handlers/prompts.tests.ts @@ -2,12 +2,11 @@ import { expect } from 'chai'; import _ from 'lodash'; import { PromisePoolExecutor } from 'promise-pool-executor'; import sinon from 'sinon'; -import axios, { AxiosResponse } from 'axios'; import promptsHandler, { Prompts } from '../../../../src/tools/auth0/handlers/prompts'; import { Language } from '../../../../src/types'; import log from '../../../../src/logger'; -import PromptsHandler, { - CustomPartialsPromptTypes, CustomPromptPartials, +import { + CustomPartialsPromptTypes, CustomPromptPartialsScreens } from '../../../../lib/tools/auth0/handlers/prompts'; @@ -87,7 +86,10 @@ describe('#prompts handler', () => { return Promise.resolve(customTextValue); }, - // updatePartials: ({ prompt ,body } ) => Promise.resolve(body), + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }) }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -154,6 +156,10 @@ describe('#prompts handler', () => { expect(data).to.deep.equal(mockPromptsSettings); return Promise.resolve(data); }, + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }) }, }; @@ -230,6 +236,10 @@ describe('#prompts handler', () => { expect(data).to.deep.equal(mockPromptsSettings); return Promise.resolve(data); }, + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }) }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -270,6 +280,10 @@ describe('#prompts handler', () => { }, prompts: { getSettings: () => mockPromptsSettings, + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }) }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -294,16 +308,6 @@ describe('#prompts handler', () => { partials: {}, // Partials empty }); }); - - it('should check if getPartialsEndpoint and putPartialsEndpoint give correct domain', () => { - const handler = new promptsHandler( - { - config: function () { return 'test-host.auth0.com'; }, - } as any); - - expect(handler.getPartialsEndpoint('login')).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); - expect(handler.putPartialsEndpoint('login')).to.equal('https://test-host.auth0.com/api/v2/prompts/login/partials'); - }); }); describe('withErrorHandling', () => { let handler: any; @@ -317,6 +321,12 @@ describe('#prompts handler', () => { return 'test-access-token'; }, }, + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }) + }, }; handler = new promptsHandler({ client: auth0 }); }); @@ -333,39 +343,44 @@ describe('#prompts handler', () => { it('should handle 403 error and set IsFeatureSupported to false', async () => { const error = { - response: { - data: { - statusCode: 403, - }, - }, + statusCode: 403, }; const callback = sandbox.stub().rejects(error); const logWarn = sandbox.stub(log, 'warn'); const result = await handler.withErrorHandling(callback); - expect(result).to.deep.equal({ data: null }); + expect(result).to.deep.equal(null); expect(handler.IsFeatureSupported).to.be.false; expect(logWarn.calledWith('Partial Prompts feature is not supported for the tenant')).to.be.true; }); it('should handle 400 error with specific message and set IsFeatureSupported to false', async () => { const error = { - response: { - data: { - statusCode: 400, - message: 'This feature requires at least one custom domain to be configured for the tenant.', - }, - }, + statusCode: 400, + message: 'This feature requires at least one custom domain to be configured for the tenant.', }; const callback = sandbox.stub().rejects(error); const logWarn = sandbox.stub(log, 'warn'); const result = await handler.withErrorHandling(callback); - expect(result).to.deep.equal({ data: null }); + expect(result).to.deep.equal(null); expect(handler.IsFeatureSupported).to.be.false; expect(logWarn.calledWith('Partial Prompts feature requires at least one custom domain to be configured for the tenant')).to.be.true; }); + it('should handle 429 error and log the appropriate message', async () => { + const error = { + statusCode: 429, + message: 'Rate limit exceeded', + }; + const callback = sandbox.stub().rejects(error); + const logError = sandbox.stub(log, 'error'); + + const result = await handler.withErrorHandling(callback); + expect(result).to.be.null; + expect(logError.calledWith(`The global rate limit has been exceeded, resulting in a ${ error.statusCode } error. ${ error.message }. Although this is an error, it is not blocking the pipeline.`)).to.be.true; + }); + it('should rethrow other errors', async () => { const error = new Error('some other error'); const callback = sandbox.stub().rejects(error); @@ -378,37 +393,30 @@ describe('#prompts handler', () => { } }); - it('should make an HTTP request with the correct headers', async () => { - const url = 'https://example.com'; - const body = { key: 'value' }; - const axiosStub = sandbox.stub(axios, 'put').resolves({ data: 'response' } as AxiosResponse); - - const result = await handler.partialHttpRequest('put', [url, body]); - expect(axiosStub.calledOnce).to.be.true; - const { args } = axiosStub.getCall(0); - expect(args[0]).to.equal(url); - expect(args[1]).to.deep.equal(body); - expect(args[2]).to.deep.include({ - headers: { - Accept: 'application/json', - Authorization: 'Bearer test-access-token', - }, - }); - expect(result).to.deep.equal({ data: 'response' }); - }); - - it('should handle errors correctly', async () => { + it('should handle errors correctly in partialHttpRequest', async () => { + const method = 'put'; + const options = [{ prompt: 'test-prompt' }, { key: 'value' }]; const error = new Error('Request failed'); - sandbox.stub(axios, 'put').rejects(error); + sandbox.stub(handler.promptClient, method).rejects(error); try { - await handler.partialHttpRequest('put', ['https://example.com', {}]); + await handler.partialHttpRequest(method, options); throw new Error('Expected method to throw.'); } catch (err) { expect(err).to.equal(error); } }); + it('should make an HTTP request with the correct headers', async () => { + const method = 'put'; + const options = [{ prompt: 'test-prompt' }, { key: 'value' }]; + const mockResponse = { data: 'response' }; + sandbox.stub(handler.promptClient, method).resolves(mockResponse); + + const result = await handler.partialHttpRequest(method, options); + expect(result).to.deep.equal(mockResponse); + }); + it('should not make a request if the feature is not supported', async () => { handler.IsFeatureSupported = false; const putStub = sandbox.stub(handler, 'partialHttpRequest'); @@ -420,9 +428,7 @@ describe('#prompts handler', () => { it('should make a request if the feature is supported', async () => { handler.IsFeatureSupported = true; - const url = 'https://example.com'; const body = { key: 'value' }; - sandbox.stub(handler, 'putPartialsEndpoint').returns(url); const putStub = sandbox.stub(handler, 'partialHttpRequest').resolves(); await handler.updateCustomPartials({ prompt: 'login', body }); @@ -430,7 +436,7 @@ describe('#prompts handler', () => { expect(putStub.calledOnce).to.be.true; const { args } = putStub.getCall(0); expect(args[0]).to.equal('put'); - expect(args[1]).to.deep.equal([url, body]); + expect(args[1]).to.deep.equal([{ prompt: 'login' }, body]); }); it('should return empty object if feature is not supported', async () => { @@ -444,27 +450,20 @@ describe('#prompts handler', () => { handler.IsFeatureSupported = true; const mockResponse = { - data: { - 'form-content-end': '
TEST
' - } - } as unknown as AxiosResponse; - - const url = 'https://test-host.auth0.com/api/v2/prompts/login/partials'; - sandbox.stub(handler, 'getPartialsEndpoint').returns(url); + 'form-content-end': '
TEST
' + }; sandbox.stub(handler, 'partialHttpRequest').resolves(mockResponse); - const result = await handler.getCustomPartial({ prompt: 'login' as CustomPartialsPromptTypes }); + const result = await handler.getCustomPartial({ prompt: 'login' }); - expect(result).to.deep.equal(mockResponse.data); - expect(handler.getPartialsEndpoint.calledOnceWith('login')).to.be.true; - expect(handler.partialHttpRequest.calledOnceWith('get', [url])).to.be.true; + expect(result).to.deep.equal(mockResponse); + expect(handler.partialHttpRequest.calledOnceWith('get', [{ prompt: 'login' }])).to.be.true; }); + it('should handle errors correctly', async () => { handler.IsFeatureSupported = true; - const url = 'https://test-host.auth0.com/api/v2/prompts/login/partials'; - sandbox.stub(handler, 'getPartialsEndpoint').returns(url); const error = new Error('Request failed'); sandbox.stub(handler, 'partialHttpRequest').rejects(error); From 4060638007602039f1f5dea8b5a40dab8178346a Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Mon, 29 Jul 2024 17:27:28 +0530 Subject: [PATCH 11/20] Added _getRestClient Mock --- src/tools/auth0/handlers/prompts.ts | 50 +++++----- src/types.ts | 5 +- test/context/directory/context.test.js | 6 ++ test/context/yaml/context.test.js | 8 +- test/tools/auth0/index.test.ts | 9 +- test/tools/auth0/validator.tests.js | 131 ++++++++++++++++++++++--- test/utils.js | 4 + 7 files changed, 167 insertions(+), 46 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 51d4f0128..904d18a9d 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -358,38 +358,32 @@ export default class PromptsHandler extends DefaultHandler { } async getCustomPromptsPartials(): Promise { - return this.client.pool + const partialsDataWithNulls = await this.client.pool .addEachTask({ - data: customPartialsPromptTypes.map((promptType) => ({ promptType })), - generator: ({ promptType }) => + data: customPartialsPromptTypes, + generator: (promptType) => this.getCustomPartial({ prompt: promptType, - }) - .then((partialsData: CustomPromptPartials) => { - if (isEmpty(partialsData)) return null; - return { - [promptType]: { - ...partialsData, - }, - }; - }), + }).then((partialsData: CustomPromptPartials) => { + if (isEmpty(partialsData)) return null; + return { promptType, partialsData }; + }), }) - .promise() - .then((partialsDataWithNulls) => - partialsDataWithNulls - .filter(Boolean) - .reduce( - ( - acc: CustomPromptPartials, - partialsData: { [prompt: string]: CustomPromptPartials } - ) => { - const [promptName] = Object.keys(partialsData); - acc[promptName] = partialsData[promptName]; - return acc; - }, - {} - ) - ); + .promise(); + + return partialsDataWithNulls.reduce( + ( + acc: CustomPromptPartials, + partialData: { promptType: string, partialsData: CustomPromptPartials } + ) => { + if (partialData) { + const { promptType, partialsData } = partialData; + acc[promptType] = partialsData; + } + return acc; + }, + {} + ); } async processChanges(assets: Assets): Promise { diff --git a/src/types.ts b/src/types.ts index 7a2338c6f..1d0103177 100644 --- a/src/types.ts +++ b/src/types.ts @@ -150,7 +150,10 @@ export type BaseAuth0APIClient = { }; }; prompts: { - _getRestClient(endpoint: string): Promise; + _getRestClient : (arg0: string) => { + get: (arg0: string) => Promise; + put: (arg0: string, arg1: any) => Promise; + }; updateCustomTextByLanguage: (arg0: { prompt: PromptTypes; language: Language; diff --git a/test/context/directory/context.test.js b/test/context/directory/context.test.js index 8d9603cf9..567f9a588 100644 --- a/test/context/directory/context.test.js +++ b/test/context/directory/context.test.js @@ -165,6 +165,12 @@ describe('#directory context validation', () => { }) ), }, + prompts:{ + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + }, actions: { getSettings: async () => new Promise((res) => res([])), }, diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 6d203f27a..3c955d8d1 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -553,7 +553,6 @@ describe('#YAML context validation', () => { it('should preserve keywords when dumping', async () => { const applyScimConfiguration = (connections) => connections; sinon.stub(ScimHandler.prototype, 'applyScimConfiguration').returns(applyScimConfiguration); - const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); const tenantFile = path.join(dir, 'tenant.yml'); @@ -603,10 +602,15 @@ describe('#YAML context validation', () => { }, ], }) + }, + prompts:{ + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }) } } ); - await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); diff --git a/test/tools/auth0/index.test.ts b/test/tools/auth0/index.test.ts index e6c04230e..89f0efbc8 100644 --- a/test/tools/auth0/index.test.ts +++ b/test/tools/auth0/index.test.ts @@ -2,7 +2,14 @@ import { expect } from 'chai'; import Auth0 from '../../../src/tools/auth0'; import { Auth0APIClient, Assets } from '../../../src/types'; -const mockEmptyClient = {} as Auth0APIClient; +const mockEmptyClient = { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } +} as Auth0APIClient; const mockEmptyAssets = {} as Assets; describe('#Auth0 class', () => { diff --git a/test/tools/auth0/validator.tests.js b/test/tools/auth0/validator.tests.js index f88a2be73..53dc4fa30 100644 --- a/test/tools/auth0/validator.tests.js +++ b/test/tools/auth0/validator.tests.js @@ -25,6 +25,12 @@ describe('#schema validation tests', () => { roles: { getAll: async () => ({ client_grants: [] }), }, + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } }; const failedCb = (done) => (err) => done(err || 'test failed'); @@ -42,7 +48,14 @@ describe('#schema validation tests', () => { }; const checkRequired = (field, data, done) => { - const auth0 = new Auth0({}, data, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, data, mockConfigFn); auth0 .validate() @@ -50,7 +63,13 @@ describe('#schema validation tests', () => { }; const checkEnum = (data, done) => { - const auth0 = new Auth0({}, data, mockConfigFn); + const auth0 = new Auth0({ prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, data, mockConfigFn); auth0 .validate() @@ -58,7 +77,14 @@ describe('#schema validation tests', () => { }; const checkTypeError = (field, expectedType, data, done) => { - const auth0 = new Auth0({}, data, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, data, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, `should be ${expectedType}`, field)); }; @@ -71,7 +97,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { branding: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { branding: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -127,7 +160,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { clientGrants: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { clientGrants: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be array')); }); @@ -163,7 +203,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { clients: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { clients: data }, mockConfigFn); auth0 .validate() @@ -256,7 +303,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { emailProvider: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { emailProvider: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -545,7 +599,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { prompts: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { prompts: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -610,7 +671,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { rules: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { rules: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should match pattern')); }); @@ -668,7 +736,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { rulesConfigs: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { rulesConfigs: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should match pattern')); }); @@ -703,7 +778,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { hooks: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { hooks: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should match pattern')); }); @@ -751,7 +833,14 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({}, { tenant: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { tenant: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -769,7 +858,14 @@ describe('#schema validation tests', () => { it('should fail validation if migrations is not an object', (done) => { const data = ''; - const auth0 = new Auth0({}, { migrations: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { migrations: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -779,7 +875,14 @@ describe('#schema validation tests', () => { migration_flag: 'string', }; - const auth0 = new Auth0({}, { migrations: data }, mockConfigFn); + const auth0 = new Auth0({ + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), + } + }, { migrations: data }, mockConfigFn); auth0.validate().then(failedCb(done), passedCb(done, 'should be boolean')); }); diff --git a/test/utils.js b/test/utils.js index 44267bf72..99fc4f5fd 100644 --- a/test/utils.js +++ b/test/utils.js @@ -120,6 +120,10 @@ export function mockMgmtClient() { }, logStreams: { getAll: () => [] }, prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), getCustomTextByLanguage: () => new Promise((res) => { res({}); From 439ac515c5fb28da971d33eed68cfb439ca6211b Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Tue, 30 Jul 2024 13:08:22 +0530 Subject: [PATCH 12/20] Fixed Put Request. --- src/tools/auth0/handlers/prompts.ts | 9 ++++-- src/types.ts | 5 ++-- test/tools/auth0/handlers/prompts.tests.ts | 35 +++++++++++++++++++--- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 904d18a9d..3e14d3a9b 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -317,9 +317,12 @@ export default class PromptsHandler extends DefaultHandler { private promptClient = this.client.prompts._getRestClient('/prompts/:prompt/partials'); private async partialHttpRequest(method: string, options: [{ prompt: string }, ...Record[]]): Promise { - return this.withErrorHandling(async () => - this.promptClient[method](...options) - ); + return this.withErrorHandling(async () =>{ + if (method === 'put') { + return this.promptClient.invoke('wrappedProvider', [method,options]); + } + return this.promptClient[method](...options); + }); } /** diff --git a/src/types.ts b/src/types.ts index 1d0103177..be6a490ef 100644 --- a/src/types.ts +++ b/src/types.ts @@ -151,8 +151,9 @@ export type BaseAuth0APIClient = { }; prompts: { _getRestClient : (arg0: string) => { - get: (arg0: string) => Promise; - put: (arg0: string, arg1: any) => Promise; + get: (arg0: string) => Promise; + put: (arg0: string, arg1: any) => Promise; + invoke: (arg0: string, arg1: any) => Promise; }; updateCustomTextByLanguage: (arg0: { prompt: PromptTypes; diff --git a/test/tools/auth0/handlers/prompts.tests.ts b/test/tools/auth0/handlers/prompts.tests.ts index 9b290285e..6507185d8 100644 --- a/test/tools/auth0/handlers/prompts.tests.ts +++ b/test/tools/auth0/handlers/prompts.tests.ts @@ -325,6 +325,7 @@ describe('#prompts handler', () => { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + invoke: (...options) => Promise.resolve({ endpoint, method: 'put', options }), }) }, }; @@ -393,9 +394,9 @@ describe('#prompts handler', () => { } }); - it('should handle errors correctly in partialHttpRequest', async () => { - const method = 'put'; - const options = [{ prompt: 'test-prompt' }, { key: 'value' }]; + it('should handle errors correctly in partialHttpRequest with Get Method', async () => { + const method = 'get'; + const options = [{ prompt: 'test-prompt' }]; const error = new Error('Request failed'); sandbox.stub(handler.promptClient, method).rejects(error); @@ -407,9 +408,25 @@ describe('#prompts handler', () => { } }); - it('should make an HTTP request with the correct headers', async () => { + + + it('should handle errors correctly in partialHttpRequest with Put Method', async () => { const method = 'put'; const options = [{ prompt: 'test-prompt' }, { key: 'value' }]; + const error = new Error('Request failed'); + sandbox.stub(handler.promptClient, 'invoke').rejects(error); + + try { + await handler.partialHttpRequest(method, options); + throw new Error('Expected method to throw.'); + } catch (err) { + expect(err).to.equal(error); + } + }); + + it('should make an HTTP request with the correct headers with Get Method', async () => { + const method = 'get'; + const options = [{ prompt: 'test-prompt' }]; const mockResponse = { data: 'response' }; sandbox.stub(handler.promptClient, method).resolves(mockResponse); @@ -417,6 +434,16 @@ describe('#prompts handler', () => { expect(result).to.deep.equal(mockResponse); }); + it('should make an HTTP request with the correct headers with Put Method', async () => { + const method = 'put'; + const options = [{ prompt: 'test-prompt' }, { key: 'value' }]; + const mockResponse = { data: 'response' }; + sandbox.stub(handler.promptClient, 'invoke').resolves(mockResponse); + + const result = await handler.partialHttpRequest(method, options); + expect(result).to.deep.equal(mockResponse); + }); + it('should not make a request if the feature is not supported', async () => { handler.IsFeatureSupported = false; const putStub = sandbox.stub(handler, 'partialHttpRequest'); From 0047e453743bf7737334535bb2f269fddc741d31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:06:09 +0530 Subject: [PATCH 13/20] Bump chai from 4.4.1 to 4.5.0 (#932) Bumps [chai](https://github.com/chaijs/chai) from 4.4.1 to 4.5.0. - [Release notes](https://github.com/chaijs/chai/releases) - [Changelog](https://github.com/chaijs/chai/blob/main/History.md) - [Commits](https://github.com/chaijs/chai/compare/v4.4.1...v4.5.0) --- updated-dependencies: - dependency-name: chai dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72a983d92..f527de493 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1687,9 +1687,9 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -1698,7 +1698,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -1716,6 +1716,15 @@ "chai": ">= 2.1.2 < 5" } }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8264,9 +8273,9 @@ "dev": true }, "chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "requires": { "assertion-error": "^1.1.0", @@ -8275,7 +8284,15 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } } }, "chai-as-promised": { From b2fa9d23319c9d7f7052430261397c2b83ce34a9 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Tue, 30 Jul 2024 16:18:02 +0530 Subject: [PATCH 14/20] FIx Formatting --- src/context/directory/handlers/prompts.ts | 67 +++-- src/tools/auth0/handlers/prompts.ts | 30 +-- src/types.ts | 21 +- test/context/directory/context.test.js | 3 +- test/context/directory/prompts.test.ts | 74 +++--- test/context/yaml/context.test.js | 15 +- test/tools/auth0/handlers/prompts.tests.ts | 76 +++--- test/tools/auth0/index.test.ts | 3 +- test/tools/auth0/validator.tests.js | 270 ++++++++++++--------- test/utils.js | 2 +- 10 files changed, 314 insertions(+), 247 deletions(-) diff --git a/src/context/directory/handlers/prompts.ts b/src/context/directory/handlers/prompts.ts index f741c8606..351364db5 100644 --- a/src/context/directory/handlers/prompts.ts +++ b/src/context/directory/handlers/prompts.ts @@ -14,16 +14,18 @@ import { CustomPromptPartialsScreens, Prompts, PromptSettings, - ScreenConfig + ScreenConfig, } from '../../../tools/auth0/handlers/prompts'; type ParsedPrompts = ParsedAsset<'prompts', Prompts>; const getPromptsDirectory = (filePath: string) => path.join(filePath, constants.PROMPTS_DIRECTORY); -const getPromptsSettingsFile = (promptsDirectory: string) => path.join(promptsDirectory, 'prompts.json'); +const getPromptsSettingsFile = (promptsDirectory: string) => + path.join(promptsDirectory, 'prompts.json'); -const getCustomTextFile = (promptsDirectory: string) => path.join(promptsDirectory, 'custom-text.json'); +const getCustomTextFile = (promptsDirectory: string) => + path.join(promptsDirectory, 'custom-text.json'); const getPartialsFile = (promptsDirectory: string) => path.join(promptsDirectory, 'partials.json'); @@ -59,14 +61,22 @@ function parse(context: DirectoryContext): ParsedPrompts { return Object.entries(partialsFileContent).reduce((acc, [promptName, screensArray]) => { const screensObject = screensArray[0] as Record; - acc[promptName as CustomPartialsPromptTypes] = Object.entries(screensObject).reduce((screenAcc, [screenName, items]) => { - screenAcc[screenName as CustomPartialsScreenTypes] = items.reduce((insertionAcc, { name, template }) => { - const templateFilePath = path.join(promptsDirectory, template); - insertionAcc[name] = isFile(templateFilePath) ? readFileSync(templateFilePath, 'utf8').trim() : ''; - return insertionAcc; - }, {} as Record); - return screenAcc; - }, {} as Record>); + acc[promptName as CustomPartialsPromptTypes] = Object.entries(screensObject).reduce( + (screenAcc, [screenName, items]) => { + screenAcc[screenName as CustomPartialsScreenTypes] = items.reduce( + (insertionAcc, { name, template }) => { + const templateFilePath = path.join(promptsDirectory, template); + insertionAcc[name] = isFile(templateFilePath) + ? readFileSync(templateFilePath, 'utf8').trim() + : ''; + return insertionAcc; + }, + {} as Record + ); + return screenAcc; + }, + {} as Record> + ); return acc; }, {} as Record>>); })(); @@ -103,18 +113,29 @@ async function dump(context: DirectoryContext): Promise { const transformedPartials = Object.entries(partials).reduce((acc, [promptName, screens]) => { acc[promptName as CustomPartialsPromptTypes] = [ - Object.entries(screens as CustomPromptPartialsScreens).reduce((screenAcc, [screenName, insertionPoints]) => { - screenAcc[screenName as CustomPartialsScreenTypes] = Object.entries(insertionPoints as Partial>).map(([insertionPoint, template]) => { - const templateFilePath = path.join(promptsDirectory, 'partials', promptName, screenName, `${insertionPoint}.liquid`); - ensureDirSync(path.dirname(templateFilePath)); - writeFileSync(templateFilePath, template, 'utf8'); - return { - name: insertionPoint, - template: path.relative(promptsDirectory, templateFilePath) // Path relative to `promptsDirectory` - }; - }); - return screenAcc; - }, {} as Record) + Object.entries(screens as CustomPromptPartialsScreens).reduce( + (screenAcc, [screenName, insertionPoints]) => { + screenAcc[screenName as CustomPartialsScreenTypes] = Object.entries( + insertionPoints as Partial> + ).map(([insertionPoint, template]) => { + const templateFilePath = path.join( + promptsDirectory, + 'partials', + promptName, + screenName, + `${insertionPoint}.liquid` + ); + ensureDirSync(path.dirname(templateFilePath)); + writeFileSync(templateFilePath, template, 'utf8'); + return { + name: insertionPoint, + template: path.relative(promptsDirectory, templateFilePath), // Path relative to `promptsDirectory` + }; + }); + return screenAcc; + }, + {} as Record + ), ]; return acc; }, {} as CustomPartialsConfig); diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 3e14d3a9b..4cc677d0e 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -238,9 +238,16 @@ export default class PromptsHandler extends DefaultHandler { private IsFeatureSupported: boolean = true; - /** - * Returns formatted endpoint url. - */ + private promptClient = this.client.prompts._getRestClient('/prompts/:prompt/partials'); + + private async partialHttpRequest(method: string, options: [{ prompt: string }, ...Record[]]): Promise { + return this.withErrorHandling(async () => { + if (method === 'put') { + return this.promptClient.invoke('wrappedProvider', [method, options]); + } + return this.promptClient[method](...options); + }); + } constructor(options: DefaultHandler) { super({ @@ -314,17 +321,6 @@ export default class PromptsHandler extends DefaultHandler { }, {})); } - private promptClient = this.client.prompts._getRestClient('/prompts/:prompt/partials'); - - private async partialHttpRequest(method: string, options: [{ prompt: string }, ...Record[]]): Promise { - return this.withErrorHandling(async () =>{ - if (method === 'put') { - return this.promptClient.invoke('wrappedProvider', [method,options]); - } - return this.promptClient[method](...options); - }); - } - /** * Error handler wrapper. */ @@ -347,7 +343,7 @@ export default class PromptsHandler extends DefaultHandler { } if (error && error.statusCode === 429) { - log.error(`The global rate limit has been exceeded, resulting in a ${ error.statusCode } error. ${ error.message }. Although this is an error, it is not blocking the pipeline.`); + log.error(`The global rate limit has been exceeded, resulting in a ${error.statusCode} error. ${error.message}. Although this is an error, it is not blocking the pipeline.`); return null; } @@ -357,7 +353,7 @@ export default class PromptsHandler extends DefaultHandler { async getCustomPartial({ prompt }: { prompt: CustomPartialsPromptTypes }): Promise { if (!this.IsFeatureSupported) return {}; - return this.partialHttpRequest('get', [{ prompt:prompt }]); // Implement this method for making HTTP requests + return this.partialHttpRequest('get', [{ prompt: prompt }]); // Implement this method for making HTTP requests } async getCustomPromptsPartials(): Promise { @@ -441,7 +437,7 @@ export default class PromptsHandler extends DefaultHandler { async updateCustomPartials({ prompt, body }: { prompt: CustomPartialsPromptTypes; body: CustomPromptPartialsScreens }): Promise { if (!this.IsFeatureSupported) return; - await this.partialHttpRequest('put', [ { prompt:prompt },body]); // Implement this method for making HTTP requests + await this.partialHttpRequest('put', [{ prompt: prompt }, body]); // Implement this method for making HTTP requests } async updateCustomPromptsPartials(partials: Prompts['partials']): Promise { diff --git a/src/types.ts b/src/types.ts index be6a490ef..8fb49014c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,15 +66,15 @@ export type BaseAuth0APIClient = { getBreachedPasswordDetectionConfig: () => Promise; getBruteForceConfig: () => Promise; getSuspiciousIpThrottlingConfig: () => Promise; - updateBreachedPasswordDetectionConfig: ({}, arg1: Asset) => Promise; - updateSuspiciousIpThrottlingConfig: ({}, arg1: Asset) => Promise; - updateBruteForceConfig: ({}, arg1: Asset) => Promise; + updateBreachedPasswordDetectionConfig: ({ }, arg1: Asset) => Promise; + updateSuspiciousIpThrottlingConfig: ({ }, arg1: Asset) => Promise; + updateBruteForceConfig: ({ }, arg1: Asset) => Promise; }; branding: APIClientBaseFunctions & { getSettings: () => Promise; getUniversalLoginTemplate: () => Promise; - updateSettings: ({}, Asset) => Promise; - setUniversalLoginTemplate: ({}, Asset) => Promise; + updateSettings: ({ }, Asset) => Promise; + setUniversalLoginTemplate: ({ }, Asset) => Promise; getDefaultTheme: () => Promise; updateTheme: ( arg0: { id: string }, @@ -150,9 +150,8 @@ export type BaseAuth0APIClient = { }; }; prompts: { - _getRestClient : (arg0: string) => { + _getRestClient: (arg0: string) => { get: (arg0: string) => Promise; - put: (arg0: string, arg1: any) => Promise; invoke: (arg0: string, arg1: any) => Promise; }; updateCustomTextByLanguage: (arg0: { @@ -237,10 +236,10 @@ export type Assets = Partial<{ actions: Action[] | null; attackProtection: Asset | null; branding: - | (Asset & { - templates?: { template: string; body: string }[] | null; - }) - | null; + | (Asset & { + templates?: { template: string; body: string }[] | null; + }) + | null; clients: Client[] | null; clientGrants: ClientGrant[] | null; connections: Asset[] | null; diff --git a/test/context/directory/context.test.js b/test/context/directory/context.test.js index 567f9a588..8da928db2 100644 --- a/test/context/directory/context.test.js +++ b/test/context/directory/context.test.js @@ -165,10 +165,9 @@ describe('#directory context validation', () => { }) ), }, - prompts:{ + prompts: { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), }), }, actions: { diff --git a/test/context/directory/prompts.test.ts b/test/context/directory/prompts.test.ts index 9f242ef9d..d11ec9455 100644 --- a/test/context/directory/prompts.test.ts +++ b/test/context/directory/prompts.test.ts @@ -5,7 +5,13 @@ import { constants } from '../../../src/tools'; import Context from '../../../src/context/directory'; import promptsHandler from '../../../src/context/directory/handlers/prompts'; import { getFiles, loadJSON } from '../../../src/utils'; -import { cleanThenMkdir, testDataDir, createDir, mockMgmtClient, createDirWithNestedDir } from '../../utils'; +import { + cleanThenMkdir, + testDataDir, + createDir, + mockMgmtClient, + createDirWithNestedDir, +} from '../../utils'; const dir = path.join(testDataDir, 'directory', 'promptsDump'); const promptsDirectory = path.join(dir, constants.PROMPTS_DIRECTORY); @@ -54,8 +60,8 @@ describe('#directory context prompts', () => { name: 'form-content-start', template: 'partials/login/login/form-content-start.liquid', }, - ] - } + ], + }, ], signup: [ { @@ -64,8 +70,8 @@ describe('#directory context prompts', () => { name: 'form-content-end', template: 'partials/signup/signup/form-content-end.liquid', }, - ] - } + ], + }, ], }), }, @@ -84,13 +90,13 @@ describe('#directory context prompts', () => { login: { login: { 'form-content-start.liquid': '
TEST
', - } + }, }, signup: { signup: { 'form-content-end.liquid': '
TEST AGAIN
', }, - } + }, }; createDirWithNestedDir(partialsDir, partialsFiles); @@ -111,13 +117,13 @@ describe('#directory context prompts', () => { login: { login: { 'form-content-start': '
TEST
', - } + }, }, signup: { signup: { 'form-content-end': '
TEST AGAIN
', }, - } + }, }, customText: { en: { @@ -273,13 +279,13 @@ describe('#directory context prompts', () => { login: { login: { 'form-content-start': '
TEST
', - } + }, }, signup: { signup: { 'form-content-end': '
TEST AGAIN
', }, - } + }, }, }; @@ -296,30 +302,28 @@ describe('#directory context prompts', () => { expect(loadJSON(path.join(promptsDirectory, customTextFile), {})).to.deep.equal( context.assets.prompts.customText ); - expect(loadJSON(path.join(promptsDirectory, partialsFile), {})).to.deep.equal( - { - login: [ - { - login: [ - { - name: 'form-content-start', - template: 'partials/login/login/form-content-start.liquid', - }, - ] - } - ], - signup: [ - { - signup: [ - { - name: 'form-content-end', - template: 'partials/signup/signup/form-content-end.liquid', - }, - ] - } - ], - } - ); + expect(loadJSON(path.join(promptsDirectory, partialsFile), {})).to.deep.equal({ + login: [ + { + login: [ + { + name: 'form-content-start', + template: 'partials/login/login/form-content-start.liquid', + }, + ], + }, + ], + signup: [ + { + signup: [ + { + name: 'form-content-end', + template: 'partials/signup/signup/form-content-end.liquid', + }, + ], + }, + ], + }); expect(loadJSON(path.join(promptsDirectory, promptsSettingsFile), {})).to.deep.equal({ universal_login_experience: context.assets.prompts.universal_login_experience, identifier_first: context.assets.prompts.identifier_first, diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 3c955d8d1..450d229e8 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -291,7 +291,7 @@ describe('#YAML context validation', () => { }, prompts: { customText: {}, - partials: {} + partials: {}, }, logStreams: [], customDomains: [], @@ -404,7 +404,7 @@ describe('#YAML context validation', () => { }, prompts: { customText: {}, - partials: {} + partials: {}, }, logStreams: [], customDomains: [], @@ -518,7 +518,7 @@ describe('#YAML context validation', () => { }, prompts: { customText: {}, - partials: {} + partials: {}, }, logStreams: [], customDomains: [], @@ -601,14 +601,13 @@ describe('#YAML context validation', () => { }, }, ], - }) + }), }, - prompts:{ + prompts: { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }) - } + }), + }, } ); await context.dump(); diff --git a/test/tools/auth0/handlers/prompts.tests.ts b/test/tools/auth0/handlers/prompts.tests.ts index 6507185d8..bd225c334 100644 --- a/test/tools/auth0/handlers/prompts.tests.ts +++ b/test/tools/auth0/handlers/prompts.tests.ts @@ -7,7 +7,7 @@ import { Language } from '../../../../src/types'; import log from '../../../../src/logger'; import { CustomPartialsPromptTypes, - CustomPromptPartialsScreens + CustomPromptPartialsScreens, } from '../../../../lib/tools/auth0/handlers/prompts'; const mockPromptsSettings = { @@ -88,8 +88,7 @@ describe('#prompts handler', () => { }, _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }) + }), }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -98,11 +97,9 @@ describe('#prompts handler', () => { }), }; - const handler = new promptsHandler( - { - client: auth0, - } - ); + const handler = new promptsHandler({ + client: auth0, + }); const getCustomPartial = sinon.stub(handler, 'getCustomPartial'); getCustomPartial.withArgs({ prompt: 'login' }).resolves(loginPartial); @@ -131,7 +128,7 @@ describe('#prompts handler', () => { }, signup: { signup: signupPartial.signup, - } + }, }, }); }); @@ -158,16 +155,13 @@ describe('#prompts handler', () => { }, _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }) + }), }, }; - const handler = new promptsHandler( - { - client: auth0, - } - ); + const handler = new promptsHandler({ + client: auth0, + }); sinon.stub(handler, 'updateCustomPartials').callsFake(() => { didCallUpdatePartials = true; return Promise.resolve({}); @@ -175,7 +169,9 @@ describe('#prompts handler', () => { const stageFn = handler.processChanges.bind(handler); const customText = undefined; - await stageFn.apply(handler, [{ prompts: { ...mockPromptsSettings, customText }, partials: undefined }]); + await stageFn.apply(handler, [ + { prompts: { ...mockPromptsSettings, customText }, partials: undefined }, + ]); expect(didCallUpdatePromptsSettings).to.equal(true); expect(didCallUpdateCustomText).to.equal(false); expect(didCallUpdatePartials).to.equal(false); @@ -238,8 +234,7 @@ describe('#prompts handler', () => { }, _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }) + }), }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -261,7 +256,9 @@ describe('#prompts handler', () => { const stageFn = Object.getPrototypeOf(handler).processChanges; await stageFn.apply(handler, [ - { prompts: { ...mockPromptsSettings, customText: customTextToSet, partials: partialsToSet } }, + { + prompts: { ...mockPromptsSettings, customText: customTextToSet, partials: partialsToSet }, + }, ]); expect(didCallUpdatePromptsSettings).to.equal(true); expect(didCallUpdateCustomText).to.equal(true); @@ -282,8 +279,7 @@ describe('#prompts handler', () => { getSettings: () => mockPromptsSettings, _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }) + }), }, pool: new PromisePoolExecutor({ concurrencyLimit: 3, @@ -324,9 +320,9 @@ describe('#prompts handler', () => { prompts: { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + invoke: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }) + }), }, }; handler = new promptsHandler({ client: auth0 }); @@ -352,13 +348,15 @@ describe('#prompts handler', () => { const result = await handler.withErrorHandling(callback); expect(result).to.deep.equal(null); expect(handler.IsFeatureSupported).to.be.false; - expect(logWarn.calledWith('Partial Prompts feature is not supported for the tenant')).to.be.true; + expect(logWarn.calledWith('Partial Prompts feature is not supported for the tenant')).to.be + .true; }); it('should handle 400 error with specific message and set IsFeatureSupported to false', async () => { const error = { statusCode: 400, - message: 'This feature requires at least one custom domain to be configured for the tenant.', + message: + 'This feature requires at least one custom domain to be configured for the tenant.', }; const callback = sandbox.stub().rejects(error); const logWarn = sandbox.stub(log, 'warn'); @@ -366,7 +364,11 @@ describe('#prompts handler', () => { const result = await handler.withErrorHandling(callback); expect(result).to.deep.equal(null); expect(handler.IsFeatureSupported).to.be.false; - expect(logWarn.calledWith('Partial Prompts feature requires at least one custom domain to be configured for the tenant')).to.be.true; + expect( + logWarn.calledWith( + 'Partial Prompts feature requires at least one custom domain to be configured for the tenant' + ) + ).to.be.true; }); it('should handle 429 error and log the appropriate message', async () => { @@ -379,7 +381,11 @@ describe('#prompts handler', () => { const result = await handler.withErrorHandling(callback); expect(result).to.be.null; - expect(logError.calledWith(`The global rate limit has been exceeded, resulting in a ${ error.statusCode } error. ${ error.message }. Although this is an error, it is not blocking the pipeline.`)).to.be.true; + expect( + logError.calledWith( + `The global rate limit has been exceeded, resulting in a ${error.statusCode} error. ${error.message}. Although this is an error, it is not blocking the pipeline.` + ) + ).to.be.true; }); it('should rethrow other errors', async () => { @@ -408,8 +414,6 @@ describe('#prompts handler', () => { } }); - - it('should handle errors correctly in partialHttpRequest with Put Method', async () => { const method = 'put'; const options = [{ prompt: 'test-prompt' }, { key: 'value' }]; @@ -448,7 +452,10 @@ describe('#prompts handler', () => { handler.IsFeatureSupported = false; const putStub = sandbox.stub(handler, 'partialHttpRequest'); - await handler.updateCustomPartials({ prompt: 'login', body: {} as CustomPromptPartialsScreens }); + await handler.updateCustomPartials({ + prompt: 'login', + body: {} as CustomPromptPartialsScreens, + }); expect(putStub.called).to.be.false; }); @@ -469,7 +476,9 @@ describe('#prompts handler', () => { it('should return empty object if feature is not supported', async () => { handler.IsFeatureSupported = false; - const result = await handler.getCustomPartial({ prompt: 'login' as CustomPartialsPromptTypes }); + const result = await handler.getCustomPartial({ + prompt: 'login' as CustomPartialsPromptTypes, + }); expect(result).to.deep.equal({}); }); @@ -477,7 +486,7 @@ describe('#prompts handler', () => { handler.IsFeatureSupported = true; const mockResponse = { - 'form-content-end': '
TEST
' + 'form-content-end': '
TEST
', }; sandbox.stub(handler, 'partialHttpRequest').resolves(mockResponse); @@ -487,7 +496,6 @@ describe('#prompts handler', () => { expect(handler.partialHttpRequest.calledOnceWith('get', [{ prompt: 'login' }])).to.be.true; }); - it('should handle errors correctly', async () => { handler.IsFeatureSupported = true; diff --git a/test/tools/auth0/index.test.ts b/test/tools/auth0/index.test.ts index 89f0efbc8..20e8bb53b 100644 --- a/test/tools/auth0/index.test.ts +++ b/test/tools/auth0/index.test.ts @@ -6,9 +6,8 @@ const mockEmptyClient = { prompts: { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), }), - } + }, } as Auth0APIClient; const mockEmptyAssets = {} as Assets; diff --git a/test/tools/auth0/validator.tests.js b/test/tools/auth0/validator.tests.js index 53dc4fa30..7d3892e4c 100644 --- a/test/tools/auth0/validator.tests.js +++ b/test/tools/auth0/validator.tests.js @@ -3,7 +3,7 @@ import Auth0 from '../../../src/tools/auth0'; import constants from '../../../src/tools/constants'; import { mockTheme } from './handlers/themes.tests'; -const mockConfigFn = () => {}; +const mockConfigFn = () => { }; describe('#schema validation tests', () => { const client = { @@ -28,9 +28,8 @@ describe('#schema validation tests', () => { prompts: { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), }), - } + }, }; const failedCb = (done) => (err) => done(err || 'test failed'); @@ -48,14 +47,17 @@ describe('#schema validation tests', () => { }; const checkRequired = (field, data, done) => { - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, data, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + data, + mockConfigFn + ); auth0 .validate() @@ -63,13 +65,17 @@ describe('#schema validation tests', () => { }; const checkEnum = (data, done) => { - const auth0 = new Auth0({ prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, data, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + data, + mockConfigFn + ); auth0 .validate() @@ -77,14 +83,17 @@ describe('#schema validation tests', () => { }; const checkTypeError = (field, expectedType, data, done) => { - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, data, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + data, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, `should be ${expectedType}`, field)); }; @@ -97,14 +106,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { branding: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { branding: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -160,14 +172,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { clientGrants: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { clientGrants: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be array')); }); @@ -203,14 +218,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { clients: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { clients: data }, + mockConfigFn + ); auth0 .validate() @@ -303,14 +321,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { emailProvider: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { emailProvider: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -599,14 +620,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { prompts: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { prompts: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -671,14 +695,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { rules: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { rules: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should match pattern')); }); @@ -736,14 +763,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { rulesConfigs: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { rulesConfigs: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should match pattern')); }); @@ -778,14 +808,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { hooks: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { hooks: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should match pattern')); }); @@ -833,14 +866,17 @@ describe('#schema validation tests', () => { }, ]; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { tenant: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { tenant: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -858,14 +894,17 @@ describe('#schema validation tests', () => { it('should fail validation if migrations is not an object', (done) => { const data = ''; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { migrations: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { migrations: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); }); @@ -875,14 +914,17 @@ describe('#schema validation tests', () => { migration_flag: 'string', }; - const auth0 = new Auth0({ - prompts: { - _getRestClient: (endpoint) => ({ - get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), - }), - } - }, { migrations: data }, mockConfigFn); + const auth0 = new Auth0( + { + prompts: { + _getRestClient: (endpoint) => ({ + get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), + }), + }, + }, + { migrations: data }, + mockConfigFn + ); auth0.validate().then(failedCb(done), passedCb(done, 'should be boolean')); }); diff --git a/test/utils.js b/test/utils.js index 99fc4f5fd..7aeba1dfd 100644 --- a/test/utils.js +++ b/test/utils.js @@ -122,7 +122,7 @@ export function mockMgmtClient() { prompts: { _getRestClient: (endpoint) => ({ get: (...options) => Promise.resolve({ endpoint, method: 'get', options }), - put: (...options) => Promise.resolve({ endpoint, method: 'put', options }), + }), getCustomTextByLanguage: () => new Promise((res) => { From f0b685c14a9646aaea78a981f7295838e40d56dd Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Tue, 30 Jul 2024 16:36:25 +0530 Subject: [PATCH 15/20] reverted prompt types --- src/tools/auth0/handlers/prompts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 4cc677d0e..ba9eae845 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -169,9 +169,9 @@ export const schema = { ...acc, [language]: { type: 'object', - properties: promptTypes.reduce((acc, customPartialsPromptTypes) => ({ + properties: promptTypes.reduce((acc, promptTypes) => ({ ...acc, - [customPartialsPromptTypes]: { + [promptTypes]: { type: 'object', properties: screenTypes.reduce((acc, screenTypes) => ({ ...acc, From a32512f596f1b2b924939a9ef906efaa2b36ff2d Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Tue, 30 Jul 2024 17:28:01 +0530 Subject: [PATCH 16/20] Fixed Linting and schema --- src/tools/auth0/handlers/prompts.ts | 148 +++++++++++++++++----------- 1 file changed, 91 insertions(+), 57 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index ba9eae845..2f6e1c8c3 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -165,46 +165,58 @@ export const schema = { }, customText: { type: 'object', - properties: languages.reduce((acc, language) => ({ - ...acc, - [language]: { - type: 'object', - properties: promptTypes.reduce((acc, promptTypes) => ({ - ...acc, - [promptTypes]: { - type: 'object', - properties: screenTypes.reduce((acc, screenTypes) => ({ - ...acc, - [screenTypes]: { + properties: languages.reduce((acc, language) => { + return { + ...acc, + [language]: { + type: 'object', + properties: promptTypes.reduce((promptAcc, promptType) => { + return { + ...promptAcc, + [promptType]: { type: 'object', + properties: screenTypes.reduce((screenAcc, screenType) => { + return { + ...screenAcc, + [screenType]: { + type: 'object', + }, + }; + }, {}), }, - }), {}), - }, - }), {}), - }, - }), {}), + }; + }, {}), + }, + }; + }, {}), }, partials: { type: 'object', - properties: customPartialsPromptTypes.reduce((acc, customPartialsPromptTypes) => ({ - ...acc, - [customPartialsPromptTypes]: { - type: 'object', - properties: customPartialsScreenTypes.reduce((acc, customPartialsScreenTypes) => ({ - ...acc, - [customPartialsScreenTypes]: { - type: 'object', - properties: customPartialsInsertionPoints.reduce((acc, customPartialsInsertionPoints) => ({ - ...acc, - [customPartialsInsertionPoints]: { - type: 'string', + properties: customPartialsPromptTypes.reduce((acc, customPartialsPromptType) => { + return { + ...acc, + [customPartialsPromptType]: { + type: 'object', + properties: customPartialsScreenTypes.reduce((screenAcc, customPartialsScreenType) => { + return { + ...screenAcc, + [customPartialsScreenType]: { + type: 'object', + properties: customPartialsInsertionPoints.reduce((insertionAcc, customPartialsInsertionPoint) => { + return { + ...insertionAcc, + [customPartialsInsertionPoint]: { + type: 'string', + }, + }; + }, {}), }, - }), {}), - }, - }), {}), - }, - }), {}), - } + }; + }, {}), + }, + }; + }, {}), + }, }, }; @@ -240,7 +252,10 @@ export default class PromptsHandler extends DefaultHandler { private promptClient = this.client.prompts._getRestClient('/prompts/:prompt/partials'); - private async partialHttpRequest(method: string, options: [{ prompt: string }, ...Record[]]): Promise { + private async partialHttpRequest( + method: string, + options: [{ prompt: string }, ...Record[]] + ): Promise { return this.withErrorHandling(async () => { if (method === 'put') { return this.promptClient.invoke('wrappedProvider', [method, options]); @@ -270,7 +285,7 @@ export default class PromptsHandler extends DefaultHandler { return { ...promptsSettings, customText, - partials + partials, }; } @@ -305,20 +320,22 @@ export default class PromptsHandler extends DefaultHandler { }), }) .promise() - .then((customTextData) => customTextData - .filter((customTextData) => customTextData !== null) - .reduce((acc: AllPromptsByLanguage, customTextItem) => { - if (customTextItem?.language === undefined) return acc; + .then((customTextData) => + customTextData + .filter((customTextData) => customTextData !== null) + .reduce((acc: AllPromptsByLanguage, customTextItem) => { + if (customTextItem?.language === undefined) return acc; - const { language, ...customTextSettings } = customTextItem; + const { language, ...customTextSettings } = customTextItem; - return { - ...acc, - [language]: acc[language] - ? { ...acc[language], ...customTextSettings } - : { ...customTextSettings }, - }; - }, {})); + return { + ...acc, + [language]: acc[language] + ? { ...acc[language], ...customTextSettings } + : { ...customTextSettings }, + }; + }, {}) + ); } /** @@ -328,7 +345,6 @@ export default class PromptsHandler extends DefaultHandler { try { return await callback(); } catch (error) { - // Extract error data if (error && error?.statusCode === 403) { log.warn('Partial Prompts feature is not supported for the tenant'); @@ -336,14 +352,22 @@ export default class PromptsHandler extends DefaultHandler { return null; } - if (error && error?.statusCode === 400 && error.message?.includes('feature requires at least one custom domain')) { - log.warn('Partial Prompts feature requires at least one custom domain to be configured for the tenant'); + if ( + error && + error?.statusCode === 400 && + error.message?.includes('feature requires at least one custom domain') + ) { + log.warn( + 'Partial Prompts feature requires at least one custom domain to be configured for the tenant' + ); this.IsFeatureSupported = false; return null; } if (error && error.statusCode === 429) { - log.error(`The global rate limit has been exceeded, resulting in a ${error.statusCode} error. ${error.message}. Although this is an error, it is not blocking the pipeline.`); + log.error( + `The global rate limit has been exceeded, resulting in a ${error.statusCode} error. ${error.message}. Although this is an error, it is not blocking the pipeline.` + ); return null; } @@ -351,7 +375,11 @@ export default class PromptsHandler extends DefaultHandler { } } - async getCustomPartial({ prompt }: { prompt: CustomPartialsPromptTypes }): Promise { + async getCustomPartial({ + prompt, + }: { + prompt: CustomPartialsPromptTypes; + }): Promise { if (!this.IsFeatureSupported) return {}; return this.partialHttpRequest('get', [{ prompt: prompt }]); // Implement this method for making HTTP requests } @@ -369,11 +397,11 @@ export default class PromptsHandler extends DefaultHandler { }), }) .promise(); - - return partialsDataWithNulls.reduce( + const validPartialsData = partialsDataWithNulls.filter(Boolean); + return validPartialsData.reduce( ( acc: CustomPromptPartials, - partialData: { promptType: string, partialsData: CustomPromptPartials } + partialData: { promptType: string; partialsData: CustomPromptPartials } ) => { if (partialData) { const { promptType, partialsData } = partialData; @@ -435,7 +463,13 @@ export default class PromptsHandler extends DefaultHandler { .promise(); } - async updateCustomPartials({ prompt, body }: { prompt: CustomPartialsPromptTypes; body: CustomPromptPartialsScreens }): Promise { + async updateCustomPartials({ + prompt, + body, + }: { + prompt: CustomPartialsPromptTypes; + body: CustomPromptPartialsScreens; + }): Promise { if (!this.IsFeatureSupported) return; await this.partialHttpRequest('put', [{ prompt: prompt }, body]); // Implement this method for making HTTP requests } From 2e4920d168bd95aac352a07f786ad3dfc17386cf Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 1 Aug 2024 20:46:55 +0530 Subject: [PATCH 17/20] Make changes in Schema for partial prompts --- src/tools/auth0/handlers/prompts.ts | 47 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 2f6e1c8c3..850d3460b 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -196,23 +196,33 @@ export const schema = { return { ...acc, [customPartialsPromptType]: { - type: 'object', - properties: customPartialsScreenTypes.reduce((screenAcc, customPartialsScreenType) => { - return { - ...screenAcc, - [customPartialsScreenType]: { - type: 'object', - properties: customPartialsInsertionPoints.reduce((insertionAcc, customPartialsInsertionPoint) => { - return { - ...insertionAcc, - [customPartialsInsertionPoint]: { - type: 'string', - }, - }; - }, {}), - }, - }; - }, {}), + oneOf: [ + { + type: 'object', + properties: customPartialsScreenTypes.reduce((screenAcc, customPartialsScreenType) => { + return { + ...screenAcc, + [customPartialsScreenType]: { + oneOf: [ + { + type: 'object', + properties: customPartialsInsertionPoints.reduce((insertionAcc, customPartialsInsertionPoint) => { + return { + ...insertionAcc, + [customPartialsInsertionPoint]: { + type: 'string', + }, + }; + }, {}), + }, + { type: 'null' } + ], + }, + }; + }, {}), + }, + { type: 'null' } + ], }, }; }, {}), @@ -420,6 +430,7 @@ export default class PromptsHandler extends DefaultHandler { const { partials, customText, ...promptSettings } = prompts; + console.log(partials); if (!isEmpty(promptSettings)) { await this.client.prompts.updateSettings({}, promptSettings); } @@ -478,7 +489,9 @@ export default class PromptsHandler extends DefaultHandler { /* Note: deletes are not currently supported */ + console.log(partials); if (!partials) return; + console.log(partials); await this.client.pool .addEachTask({ data: Object.keys(partials).map((prompt: CustomPartialsPromptTypes) => { From dcc455fa6d54526a0ae68584935e3c2f5691e755 Mon Sep 17 00:00:00 2001 From: stevenwong-okta Date: Wed, 31 Jul 2024 14:03:45 +0800 Subject: [PATCH 18/20] Update codeowner file with new GitHub team name (#931) Co-authored-by: KunalOfficial <35455566+developerkunal@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef957b7c5..71ed0fd3d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-customer-dev-tools-engineer +* @auth0/project-dx-sdks-engineer-codeowner From 7ba84b44fe03f5158c3eaec66bcbe8ff2b50885a Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 1 Aug 2024 21:30:09 +0530 Subject: [PATCH 19/20] Removed consolelogs --- .github/CODEOWNERS | 2 +- src/tools/auth0/handlers/prompts.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 71ed0fd3d..ef957b7c5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/project-dx-sdks-engineer-codeowner +* @auth0/dx-customer-dev-tools-engineer diff --git a/src/tools/auth0/handlers/prompts.ts b/src/tools/auth0/handlers/prompts.ts index 850d3460b..9c56ba1d6 100644 --- a/src/tools/auth0/handlers/prompts.ts +++ b/src/tools/auth0/handlers/prompts.ts @@ -430,7 +430,6 @@ export default class PromptsHandler extends DefaultHandler { const { partials, customText, ...promptSettings } = prompts; - console.log(partials); if (!isEmpty(promptSettings)) { await this.client.prompts.updateSettings({}, promptSettings); } @@ -489,9 +488,7 @@ export default class PromptsHandler extends DefaultHandler { /* Note: deletes are not currently supported */ - console.log(partials); if (!partials) return; - console.log(partials); await this.client.pool .addEachTask({ data: Object.keys(partials).map((prompt: CustomPartialsPromptTypes) => { From a6c735de3ad4d47d203994a828e0d3959fa445ac Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Thu, 1 Aug 2024 21:31:45 +0530 Subject: [PATCH 20/20] Fix back codeowners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef957b7c5..71ed0fd3d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-customer-dev-tools-engineer +* @auth0/project-dx-sdks-engineer-codeowner