diff --git a/.changeset/plenty-plums-camp.md b/.changeset/plenty-plums-camp.md new file mode 100644 index 0000000000..65b973f543 --- /dev/null +++ b/.changeset/plenty-plums-camp.md @@ -0,0 +1,5 @@ +--- +'@commercetools-frontend/cypress': minor +--- + +Add more options to `loginByOidc` command. diff --git a/packages/cypress/README.md b/packages/cypress/README.md index cb46f45556..4b832485b0 100644 --- a/packages/cypress/README.md +++ b/packages/cypress/README.md @@ -31,9 +31,15 @@ module.exports = (on, config) => { Add this line to your project's `cypress/support/commands.js`: ```javascript -import '@commercetools-frontend/cypress/add-commands' +import '@commercetools-frontend/cypress/add-commands'; ``` ### Commands -* `cy.loginByOidc({ entryPointUriPath })` +- `cy.loginByOidc({ entryPointUriPath })` + + This command perform the user login using the OIDC workflow to retrieve the session token.
+ The command also requires to load the `custom-application-config.json` (automatically done via the Cypress task) and therefore it may need to load environment variables in case the application config uses environment placeholders.
+ By default, the `.env` and `.env.local` files are attempted to be loaded from the application folder. You can pass a `dotfiles` option to pass a list of names/paths relative to the application folder in case the files in the project have a different name/location. + + > The command also requires the following environment variables to be available: `PROJECT_KEY`, `LOGIN_USER`, `LOGIN_PASSWORD`. diff --git a/packages/cypress/src/add-commands/index.ts b/packages/cypress/src/add-commands/index.ts index 6059b2873d..fe2c65332d 100644 --- a/packages/cypress/src/add-commands/index.ts +++ b/packages/cypress/src/add-commands/index.ts @@ -9,16 +9,61 @@ declare const Cypress: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any declare const cy: any; +type CommandLoginByOidcOptions = { + /** + * The application entry point URI path. This value is used to identify + * the correct application config. + */ + entryPointUriPath: string; + /** + * Pass a list of dotfiles that must be loaded when the custom-application-config.json + * is loaded (in case you are using environment placeholder). + * By default the following dotfiles are loaded: `.env` and `.env.local`. + * You can also define the values using paths relative to the application folder. + */ + dotfiles?: string[]; + /** + * Called before your page has loaded all of its resources. + * Use this as a chance to interact for example with the browser storage. + */ + onBeforeLoad?: (win: Window) => void; + /** + * If defined, visit this route after login. + */ + initialRoute?: string; + /** + * The project key to access in the user session. The session token is valid for one project key at a time. + * Defaults to `Cypress.env('PROJECT_KEY')`. + */ + projectKey?: string; + /** + * The user login credentials. + */ + login?: { + /** + * The user email. + * Defaults to `Cypress.env('LOGIN_EMAIL') ?? Cypress.env('LOGIN_USER')`. + */ + email: string; + /** + * The user password. + * Defaults to `Cypress.env('LOGIN_PASSWORD')`. + */ + password: string; + }; +}; + Cypress.Commands.add( 'loginByOidc', - ({ entryPointUriPath }: { entryPointUriPath: string }) => { + (commandOptions: CommandLoginByOidcOptions) => { Cypress.log({ name: 'loginByOidc' }); - const projectKey = Cypress.env('PROJECT_KEY'); + const projectKey = commandOptions.projectKey ?? Cypress.env('PROJECT_KEY'); const sessionNonce = uuidv4(); cy.task('customApplicationConfig', { - entryPointUriPath, + entryPointUriPath: commandOptions.entryPointUriPath, + dotfiles: commandOptions.dotfiles, }).then((appConfig: ApplicationConfig['env']) => { const applicationId = appConfig.applicationId; const sessionScope = buildOidcScope({ @@ -26,14 +71,17 @@ Cypress.Commands.add( oAuthScopes: appConfig.__DEVELOPMENT__?.oAuthScopes, teamId: appConfig.__DEVELOPMENT__?.teamId, }); + const userCredentials = commandOptions.login ?? { + email: Cypress.env('LOGIN_EMAIL') ?? Cypress.env('LOGIN_USER'), + password: Cypress.env('LOGIN_PASSWORD'), + }; // Perform the login using the API, then store some required values into the browser storage // and redirect to the auth callback route. - const options = { + const requestOptions = { method: 'POST', url: `${appConfig.mcApiUrl}/tokens`, body: { - email: Cypress.env('LOGIN_USER'), - password: Cypress.env('LOGIN_PASSWORD'), + ...userCredentials, client_id: applicationId, response_type: OIDC_RESPONSE_TYPES.ID_TOKEN, scope: sessionScope, @@ -42,24 +90,37 @@ Cypress.Commands.add( }, followRedirect: false, }; - cy.request(options).then((res: { body: { redirectTo: string } }) => { - cy.visit(res.body.redirectTo, { - onBeforeLoad(win: Window) { - win.localStorage.setItem( - STORAGE_KEYS.ACTIVE_PROJECT_KEY, - projectKey - ); - win.sessionStorage.setItem( - `${STORAGE_KEYS.NONCE}_${sessionNonce}`, - JSON.stringify({ applicationId, query: {} }) - ); - win.sessionStorage.setItem( - STORAGE_KEYS.SESSION_SCOPE, - sessionScope + cy.request(requestOptions).then( + (res: { body: { redirectTo: string } }) => { + cy.visit(res.body.redirectTo, { + onBeforeLoad(win: Window) { + win.localStorage.setItem( + STORAGE_KEYS.ACTIVE_PROJECT_KEY, + projectKey + ); + win.sessionStorage.setItem( + `${STORAGE_KEYS.NONCE}_${sessionNonce}`, + JSON.stringify({ applicationId, query: {} }) + ); + win.sessionStorage.setItem( + STORAGE_KEYS.SESSION_SCOPE, + sessionScope + ); + + if (commandOptions.onBeforeLoad) { + commandOptions.onBeforeLoad(win); + } + }, + }); + + if (commandOptions.initialRoute) { + cy.visit( + `${Cypress.config('baseUrl')}${commandOptions.initialRoute}` ); - }, - }); - }); + cy.url().should('include', commandOptions.initialRoute); + } + } + ); }); } ); diff --git a/packages/cypress/src/task/index.ts b/packages/cypress/src/task/index.ts index 6655d35f9e..92c6aaf205 100644 --- a/packages/cypress/src/task/index.ts +++ b/packages/cypress/src/task/index.ts @@ -10,14 +10,19 @@ import { processConfig } from '@commercetools-frontend/application-config'; type CustomApplicationConfigTaskOptions = { entryPointUriPath: string; + dotfiles?: string[]; }; type AllCustomApplicationConfigs = Record; let cachedAllCustomApplicationConfigs: AllCustomApplicationConfigs; -// TODO: make it configurable? -const dotfiles = ['.env', '.env.local']; -const loadEnvironmentVariables = (packageDirPath: string) => { +const defaultDotfiles = ['.env', '.env.local']; + +const loadEnvironmentVariables = ( + packageDirPath: string, + options: CustomApplicationConfigTaskOptions +) => { + const dotfiles = options.dotfiles ?? defaultDotfiles; return dotfiles.reduce((mergedEnvs, dotfile) => { const envPath = path.join(packageDirPath, dotfile); @@ -40,7 +45,9 @@ const loadEnvironmentVariables = (packageDirPath: string) => { }, process.env); }; -const loadAllCustomApplicationConfigs = async () => { +const loadAllCustomApplicationConfigs = async ( + options: CustomApplicationConfigTaskOptions +) => { if (cachedAllCustomApplicationConfigs) { return cachedAllCustomApplicationConfigs; } @@ -62,7 +69,7 @@ const loadAllCustomApplicationConfigs = async () => { const customAppConfigJson: TJSONSchemaForCustomApplicationConfigurationFiles = JSON.parse( fs.readFileSync(customAppConfigPath, { encoding: 'utf8' }) ); - const processEnv = loadEnvironmentVariables(packageInfo.dir); + const processEnv = loadEnvironmentVariables(packageInfo.dir, options); const processedConfig = processConfig({ disableCache: true, configJson: customAppConfigJson, @@ -79,17 +86,19 @@ const loadAllCustomApplicationConfigs = async () => { return cachedAllCustomApplicationConfigs; }; -const customApplicationConfig = async ({ - entryPointUriPath, -}: CustomApplicationConfigTaskOptions): Promise => { - const allCustomApplicationConfigs = await loadAllCustomApplicationConfigs(); +const customApplicationConfig = async ( + options: CustomApplicationConfigTaskOptions +): Promise => { + const allCustomApplicationConfigs = await loadAllCustomApplicationConfigs( + options + ); const customApplicationConfig = - allCustomApplicationConfigs[entryPointUriPath]; + allCustomApplicationConfigs[options.entryPointUriPath]; if (!customApplicationConfig) { throw new Error( - `Could not find Custom Application config for entry point "${entryPointUriPath}"` + `Could not find Custom Application config for entry point "${options.entryPointUriPath}"` ); }