Skip to content

Commit

Permalink
feat(cypress): add more options to loginByOidc command
Browse files Browse the repository at this point in the history
  • Loading branch information
emmenko committed Jan 27, 2021
1 parent 7a53760 commit d1efaf0
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-plums-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools-frontend/cypress': minor
---

Add more options to `loginByOidc` command.
10 changes: 8 additions & 2 deletions packages/cypress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br/>
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.<br/>
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`.
107 changes: 84 additions & 23 deletions packages/cypress/src/add-commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,79 @@ 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({
projectKey,
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,
Expand All @@ -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);
}
}
);
});
}
);
31 changes: 20 additions & 11 deletions packages/cypress/src/task/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ import { processConfig } from '@commercetools-frontend/application-config';

type CustomApplicationConfigTaskOptions = {
entryPointUriPath: string;
dotfiles?: string[];
};
type AllCustomApplicationConfigs = Record<string, ApplicationConfig['env']>;

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);

Expand All @@ -40,7 +45,9 @@ const loadEnvironmentVariables = (packageDirPath: string) => {
}, process.env);
};

const loadAllCustomApplicationConfigs = async () => {
const loadAllCustomApplicationConfigs = async (
options: CustomApplicationConfigTaskOptions
) => {
if (cachedAllCustomApplicationConfigs) {
return cachedAllCustomApplicationConfigs;
}
Expand All @@ -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,
Expand All @@ -79,17 +86,19 @@ const loadAllCustomApplicationConfigs = async () => {
return cachedAllCustomApplicationConfigs;
};

const customApplicationConfig = async ({
entryPointUriPath,
}: CustomApplicationConfigTaskOptions): Promise<ApplicationConfig['env']> => {
const allCustomApplicationConfigs = await loadAllCustomApplicationConfigs();
const customApplicationConfig = async (
options: CustomApplicationConfigTaskOptions
): Promise<ApplicationConfig['env']> => {
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}"`
);
}

Expand Down

0 comments on commit d1efaf0

Please sign in to comment.