Skip to content

Commit

Permalink
support okta with cypress tests (jhipster#13611)
Browse files Browse the repository at this point in the history
* support okta with cypress tests

closes jhipster#12686

* correctly guard oauth specific functions/utils

* append login/logout paths correctly to baseUrl

* use keycloak api and only pupeteer when using okta

* fix oauth2 data creation

* add missing keycloakLogin function

* Apply suggestions from code review

Co-authored-by: Marcelo Shima <marceloshima@gmail.com>

* correct identation

* replace deprecated waitFor with waitforSelector

Co-authored-by: Marcelo Shima <marceloshima@gmail.com>
  • Loading branch information
2 people authored and gkysaad committed Jun 1, 2021
1 parent f19b40f commit 36ad5f3
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 106 deletions.
3 changes: 3 additions & 0 deletions generators/client/templates/angular/package.json.ejs
Expand Up @@ -87,6 +87,9 @@
<%_ } _%>
<%_ if (cypressTests) { _%>
"cypress": "VERSION_MANAGED_BY_CLIENT_COMMON",
<%_ if (authenticationType === 'oauth2') { _%>
"puppeteer": "VERSION_MANAGED_BY_CLIENT_COMMON",
<%_ } _%>
<%_ } _%>
<%_ if (enableTranslation) { _%>
"merge-jsons-webpack-plugin": "VERSION_MANAGED_BY_CLIENT_ANGULAR",
Expand Down
3 changes: 3 additions & 0 deletions generators/client/templates/react/package.json.ejs
Expand Up @@ -163,6 +163,9 @@
<%_ } _%>
<%_ if (cypressTests) { _%>
"cypress": "VERSION_MANAGED_BY_CLIENT_COMMON",
<%_ if (authenticationType === 'oauth2') { _%>
"puppeteer": "VERSION_MANAGED_BY_CLIENT_COMMON",
<%_ } _%>
<%_ } _%>
"typescript": "VERSION_MANAGED_BY_CLIENT_REACT",
<%_ if (protractorTests) { _%>
Expand Down
3 changes: 3 additions & 0 deletions generators/client/templates/vue/package.json.ejs
Expand Up @@ -119,6 +119,9 @@
<%_ } _%>
<%_ if (cypressTests) { _%>
"cypress": "VERSION_MANAGED_BY_CLIENT_COMMON",
<%_ if (authenticationType === 'oauth2') { _%>
"puppeteer": "VERSION_MANAGED_BY_CLIENT_COMMON",
<%_ } _%>
<%_ } _%>
"rimraf": "VERSION_MANAGED_BY_CLIENT_VUE",
"sass": "VERSION_MANAGED_BY_CLIENT_VUE",
Expand Down
3 changes: 1 addition & 2 deletions generators/cypress/files.js
Expand Up @@ -42,7 +42,6 @@ const cypressFiles = {
condition: generator => generator.cypressTests,
path: TEST_SRC_DIR,
templates: [
'cypress/fixtures/users/user.json',
'cypress/fixtures/integration-test.png',
'cypress/plugins/index.ts',
'cypress/integration/administration/administration.spec.ts',
Expand Down Expand Up @@ -71,7 +70,7 @@ const cypressFiles = {
{
condition: generator => generator.cypressTests && generator.authenticationType === 'oauth2',
path: TEST_SRC_DIR,
templates: ['cypress/support/keycloak-oauth2.ts'],
templates: ['cypress/support/oauth2.ts'],
},
],
};
Expand Down
6 changes: 6 additions & 0 deletions generators/cypress/index.js
Expand Up @@ -93,6 +93,12 @@ module.exports = class extends BaseBlueprintGenerator {
// Public API method used by the getter and also by Blueprints
_writing() {
return {
cleanup() {
if (this.isJhipsterVersionLessThan('7.0.0-beta.1') && this.jhipsterConfig.cypressTests) {
this.removeFile(`${this.TEST_SRC_DIR}/cypress/support/keycloak-oauth2.ts`);
this.removeFile(`${this.TEST_SRC_DIR}/cypress/fixtures/users/user.json`);
}
},
...writeFiles(),
...super._missingPostWriting(),
};
Expand Down
Expand Up @@ -15,13 +15,13 @@ describe('/admin', () => {
beforeEach(() => {
cy.getOauth2Data();
cy.get('@oauth2Data').then(oauth2Data => {
cy.keycloackLogin(oauth2Data, 'user');
cy.oauthLogin(oauth2Data, Cypress.env('E2E_USERNAME') || "admin", Cypress.env('E2E_PASSWORD') || "admin");
});
});

afterEach(() => {
cy.get('@oauth2Data').then(oauth2Data => {
cy.keycloackLogout(oauth2Data);
cy.oauthLogout(oauth2Data);
});
cy.clearCache();
});
Expand Down
Expand Up @@ -10,7 +10,9 @@

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

<%_ if (authenticationType === 'oauth2') { _%>
const puppeteer = require('puppeteer');
<%_ } _%>
/**
* @type {Cypress.PluginConfig}
*/
Expand All @@ -23,4 +25,47 @@ module.exports = (on, config) => {
return launchOptions
}
});
<%_ if (authenticationType === 'oauth2') { _%>
on('task', {
login({ baseUrl, username, password }) {
return (async () => {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
devtools: false,
ignoreHTTPSErrors: true,
headless: true });

const page = await browser.newPage();

await page.goto(`${baseUrl}oauth2/authorization/oidc`, { // The app redirects to the login-page
waitUntil: 'networkidle2' // Wait until login-page has been reached
});

await page.waitForSelector('[name="username"]');
await page.waitForSelector('[name="password"]');
await page.waitForSelector('input[type="submit"]');
await page.type('[name="username"]', username);
await page.type('[name="password"]', password);
await page.click('input[type="submit"]');

await page.waitForNavigation({ waitUntil: 'networkidle2' }); // Wait until redirected back to the app

const authCookies = await page.cookies();

browser.close();
return authCookies;
})();
},
logout(baseUrl) {
return (async () => {
const browser = await puppeteer.launch({ ignoreHTTPSErrors: true });
const page = await browser.newPage();
await page.goto(`${baseUrl}api/logout`, {
waitUntil: 'networkidle2'
});
return true;
})
}
});
<%_ } _%>
}
Expand Up @@ -17,7 +17,7 @@ import './commands';
import './navbar';
import './entity';
<%_ if (authenticationType === 'oauth2') { _%>
import './keycloak-oauth2';
import './oauth2';
<%_ } _%>
<%_ if (authenticationType === 'session') { _%>
Cypress.Cookies.defaults({
Expand Down

This file was deleted.

@@ -0,0 +1,141 @@
/* eslint-disable @typescript-eslint/camelcase */
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/no-use-before-define */
// eslint-disable-next-line spaced-comment
/// <reference types="cypress" />

Cypress.Commands.add('getOauth2Data', () => {
cy.request({
method: 'GET',
url: '/oauth2/authorization/oidc',
followRedirect: false,
}).then(response => {
const url = new URL(response.headers['location']);
const clientId = url.searchParams.get('client_id');
const responseType = url.searchParams.get('response_type');
const redirectUri = url.searchParams.get('redirect_uri');
const nonce = url.searchParams.get("nonce");
const state = url.searchParams.get("state");
const scope = url.searchParams.get("scope");
const origin = url.origin;
const authenticationUrl = `${origin}${url.pathname}`;
const isOkta = origin.includes("okta")

let logoutUrl = "";
if (isOkta) {
logoutUrl = `${origin}${url.pathname.split('/',2).join('/')}/v1/logout`
} else {
logoutUrl = `${origin}${url.pathname.split('/', 4).join('/')}/protocol/openid-connect/logout`
}

const data = {
url,
clientId,
responseType,
scope,
redirectUri,
nonce,
state,
authenticationUrl,
logoutUrl,
isOkta
};
cy.wrap(data).as('oauth2Data');
});
});

Cypress.Commands.add('oauthLogin', (oauth2Data: any, username: string, password: string) => {
if (oauth2Data.isOkta) {
cy.oktaLogin(username, password);
} else {
cy.keycloakLogin(oauth2Data, username, password);
}
});

Cypress.Commands.add('keycloakLogin', (oauth2Data: any, username: string, password: string) => {
cy.request({
url: `${oauth2Data.url}`,
followRedirect: false,
})
.then(response => {
const html = document.createElement('html');
html.innerHTML = response.body;

const form = html.getElementsByTagName('form')[0];
const url = form.action;
return cy.request({
method: 'POST',
url,
followRedirect: false,
form: true,
body: {
username: username,
password: password,
},
});
})
.then(() => {
cy.request({
method: 'GET',
url: '/oauth2/authorization/oidc',
followRedirect: true,
}).then(() => {
cy.visit('/');
});
})
});

Cypress.Commands.add('oktaLogin', (username, password) => {
cy.task('login', {
baseUrl: Cypress.config().baseUrl,
username: username,
password: password
})
.then((cookies: Array<any>) => {
cookies.forEach(c => {
const name = c.name;
const value = c.value;
const options = {
...c
}
cy.setCookie(name, value, options);
});
cy.visit('/');
});
});

Cypress.Commands.add('oauthLogout', (oauth2Data: any) => {
if (oauth2Data.isOkta) {
cy.task('logout').then(cookies => {
cy.clearCookies();
});
} else {
return cy.request({
url: `${oauth2Data.logoutUrl}`,
});
}
});

Cypress.Commands.add('clearCache', () => {
cy.clearCookies();
cy.clearLocalStorage();
cy.window().then(win => {
win.sessionStorage.clear();
});
});

declare global {
namespace Cypress {
interface Chainable<Subject> {
getOauth2Data(): Cypress.Chainable;
oauthLogin(oauth2Data: any, username: string, password: string): Cypress.Chainable;
keycloakLogin(oauth2Data: any, username: string, password: string): Cypress.Chainable;
oktaLogin(username: string, password: string): Cypress.Chainable;
oauthLogout(oauth2Data: any): Cypress.Chainable;
clearCache(): Cypress.Chainable;
}
}
}

// Convert this to a module instead of script (allows import/export)
export {};

0 comments on commit 36ad5f3

Please sign in to comment.