keycloak/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java

388 lines
14 KiB
Java

package org.keycloak.testsuite.util.javascript;
import org.jboss.logging.Logger;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.WaitUtils.pause;
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
/**
* @author mhajas
*/
public class JavascriptTestExecutor {
protected WebDriver jsDriver;
protected JavascriptExecutor jsExecutor;
private WebElement output;
protected WebElement events;
private OIDCLogin loginPage;
protected boolean configured;
private static final Logger logger = Logger.getLogger(JavascriptTestExecutor.class);
public static JavascriptTestExecutor create(WebDriver driver, OIDCLogin loginPage) {
return new JavascriptTestExecutor(driver, loginPage);
}
protected JavascriptTestExecutor(WebDriver driver, OIDCLogin loginPage) {
this.jsDriver = driver;
driver.manage().timeouts().setScriptTimeout(WaitUtils.PAGELOAD_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
jsExecutor = (JavascriptExecutor) driver;
events = driver.findElement(By.id("events"));
output = driver.findElement(By.id("output"));
this.loginPage = loginPage;
configured = false;
}
public JavascriptTestExecutor login() {
return login((String)null, null);
}
public JavascriptTestExecutor login(JavascriptStateValidator validator) {
return login((String)null, validator);
}
/**
* Attaches a MutationObserver that sends a message from iframe to main window with incorrect data when the iframe is loaded
*/
public JavascriptTestExecutor attachCheck3pCookiesIframeMutationObserver() {
jsExecutor.executeScript("// Select the node that will be observed for mutations\n" +
" const targetNode = document.body;" +
"" +
" // Options for the observer (which mutations to observe)\n" +
" const config = {attributes: true, childList: true, subtree: true};" +
"" +
" // Callback function to execute when mutations are observed\n" +
" const callback = function (mutationsList, observer) {" +
" console.log(\"Mutation found\");" +
" var iframeNode = mutationsList[0].addedNodes[0];" +
" if (iframeNode && iframeNode.localName === 'iframe') {" +
" var s = document.createElement('script');" +
" s.type = 'text/javascript';" +
" var code = \"window.parent.postMessage('Evil Message', '*');\";" +
" s.appendChild(document.createTextNode(code));" +
" iframeNode.contentDocument.body.appendChild(s);" +
" }" +
" }\n" +
"" +
" // Create an observer instance linked to the callback function\n" +
" const observer = new MutationObserver(callback);" +
"" +
" // Start observing the target node for configured mutations\n" +
" observer.observe(targetNode, config);");
return this;
}
public JavascriptTestExecutor login(JSObjectBuilder optionsBuilder, JavascriptStateValidator validator) {
return login(optionsBuilder.build(), validator);
}
public JavascriptTestExecutor login(String options, JavascriptStateValidator validator) {
if (options == null)
jsExecutor.executeScript("keycloak.login()");
else {
jsExecutor.executeScript("keycloak.login(" + options + ")");
}
waitForPageToLoad();
if (validator != null) {
validator.validate(jsDriver, output, events);
}
configured = false; // Getting out of testApp page => loosing keycloak variable etc.
return this;
}
public JavascriptTestExecutor loginForm(UserRepresentation user) {
return loginForm(user, null);
}
public JavascriptTestExecutor loginForm(UserRepresentation user, JavascriptStateValidator validator) {
loginPage.form().login(user);
waitForPageToLoad();
if (validator != null) {
validator.validate(jsDriver, null, events);
}
configured = false; // Getting out of testApp page => loosing keycloak variable etc.
// this is necessary in case we skipped login button for example in login-required mode
return this;
}
public JavascriptTestExecutor logout() {
return logout(null);
}
public JavascriptTestExecutor logout(JavascriptStateValidator validator) {
return logout(validator, null);
}
public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage) {
jsExecutor.executeScript("keycloak.logout()");
try {
// simple check if we are at the logout confirm page, if so just click 'Yes'
if (logoutConfirmPage != null && logoutConfirmPage.isCurrent(jsDriver)) {
logoutConfirmPage.confirmLogout(jsDriver);
waitForPageToLoad();
}
} catch (Exception ex) {
// ignore errors when checking logoutConfirm page, if an error tests will also fail
logger.error("Exception during checking logout confirmation page", ex);
}
if (validator != null) {
validator.validate(jsDriver, output, events);
}
configured = false; // Loosing keycloak variable so we need to create it when init next session
return this;
}
public JavascriptTestExecutor configure() {
return configure(null);
}
public JavascriptTestExecutor configure(JSObjectBuilder argumentsBuilder) {
// a nasty hack: redirect console.warn to events
// mainly for FF as it doesn't yet support reading console.warn directly through webdriver
// see https://github.com/mozilla/geckodriver/issues/284
jsExecutor.executeScript("console.warn = event;");
if (argumentsBuilder == null) {
jsExecutor.executeScript("window.keycloak = new Keycloak();");
} else {
String configArguments = argumentsBuilder.build();
jsExecutor.executeScript("window.keycloak = new Keycloak(" + configArguments + ");");
}
jsExecutor.executeScript("window.keycloak.onAuthSuccess = function () {event('Auth Success')};"); // event function is declared in index.html
jsExecutor.executeScript("window.keycloak.onAuthError = function () {event('Auth Error')}");
jsExecutor.executeScript("window.keycloak.onAuthRefreshSuccess = function () {event('Auth Refresh Success')}");
jsExecutor.executeScript("window.keycloak.onAuthRefreshError = function () {event('Auth Refresh Error')}");
jsExecutor.executeScript("window.keycloak.onAuthLogout = function () {event('Auth Logout')}");
jsExecutor.executeScript("window.keycloak.onTokenExpired = function () {event('Access token expired.')}");
jsExecutor.executeScript("window.keycloak.onActionUpdate = function (status) {event('AIA status: ' + status)}");
configured = true;
return this;
}
public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder) {
return init(argumentsBuilder, null);
}
public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder, JavascriptStateValidator validator) {
return init(argumentsBuilder, validator, false);
}
public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder, JavascriptStateValidator validator, boolean expectPromptNoneRedirect) {
if(!configured) {
configure();
}
String arguments = argumentsBuilder.build();
String script = "var callback = arguments[arguments.length - 1];" +
" window.keycloak.init(" + arguments + ").then(function (authenticated) {" +
" callback(\"Init Success (\" + (authenticated ? \"Authenticated\" : \"Not Authenticated\") + \")\");" +
" }).catch(function (error) {" +
" callback(error);" +
" });";
Object output;
if (expectPromptNoneRedirect) {
try {
output = jsExecutor.executeAsyncScript(script);
fail("Redirect to Keycloak was expected");
}
catch (WebDriverException e) {
waitForPageToLoad();
configured = false;
// the redirect should use prompt=none, that means KC should immediately redirect back to the app (regardless login state)
return init(argumentsBuilder, validator, false);
}
}
else {
output = jsExecutor.executeAsyncScript(script);
}
if (validator != null) {
validator.validate(jsDriver, output, events);
}
return this;
}
public JavascriptTestExecutor logInAndInit(JSObjectBuilder argumentsBuilder,
UserRepresentation user, JavascriptStateValidator validator) {
init(argumentsBuilder);
login();
loginForm(user);
init(argumentsBuilder, validator);
return this;
}
public JavascriptTestExecutor refreshToken(int value) {
return refreshToken(value, null);
}
public JavascriptTestExecutor refreshToken(int value, JavascriptStateValidator validator) {
String script = "var callback = arguments[arguments.length - 1];" +
" window.keycloak.updateToken(" + Integer.toString(value) + ").then(function (refreshed) {" +
" if (refreshed) {" +
" callback(window.keycloak.tokenParsed);" +
" } else {" +
" callback('Token not refreshed, valid for ' + Math.round(window.keycloak.tokenParsed.exp + window.keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');" +
" }" +
" }).catch(function () {" +
" callback('Failed to refresh token');" +
" });";
Object output = jsExecutor.executeAsyncScript(script);
if(validator != null) {
validator.validate(jsDriver, output, events);
}
return this;
}
public JavascriptTestExecutor openAccountPage(JavascriptStateValidator validator) {
jsExecutor.executeScript("window.keycloak.accountManagement()");
waitForPageToLoad();
// Leaving page -> loosing keycloak variable
configured = false;
if (validator != null) {
validator.validate(jsDriver, null, null);
}
return this;
}
public JavascriptTestExecutor getProfile() {
return getProfile(null);
}
public JavascriptTestExecutor getProfile(JavascriptStateValidator validator) {
String script = "var callback = arguments[arguments.length - 1];" +
" window.keycloak.loadUserProfile().then(function (profile) {" +
" callback(profile);" +
" }, function () {" +
" callback('Failed to load profile');" +
" });";
Object output = jsExecutor.executeAsyncScript(script);
if(validator != null) {
validator.validate(jsDriver, output, events);
}
return this;
}
public JavascriptTestExecutor sendXMLHttpRequest(XMLHttpRequest request, ResponseValidator validator) {
validator.validate(request.send(jsExecutor));
return this;
}
public JavascriptTestExecutor refresh() {
jsDriver.navigate().refresh();
configured = false; // Refreshing webpage => Loosing window.keycloak variable
return this;
}
public JavascriptTestExecutor addTimeSkew(int addition) {
jsExecutor.executeScript("window.keycloak.timeSkew += " + Integer.toString(addition));
return this;
}
public JavascriptTestExecutor checkTimeSkew(JavascriptStateValidator validator) {
Object timeSkew = jsExecutor.executeScript("return window.keycloak.timeSkew");
validator.validate(jsDriver, timeSkew, events);
return this;
}
public JavascriptTestExecutor executeScript(String script) {
return executeScript(script, null);
}
public JavascriptTestExecutor executeScript(String script, JavascriptStateValidator validator) {
Object output = jsExecutor.executeScript(script);
if(validator != null) {
validator.validate(jsDriver, output, events);
}
return this;
}
public boolean isLoggedIn() {
return (boolean) jsExecutor.executeScript("if (typeof keycloak !== 'undefined') {" +
"return keycloak.authenticated" +
"} else { return false}");
}
public JavascriptTestExecutor executeAsyncScript(String script) {
return executeAsyncScript(script, null);
}
public JavascriptTestExecutor executeAsyncScript(String script, JavascriptStateValidator validator) {
Object output = jsExecutor.executeAsyncScript(script);
if(validator != null) {
validator.validate(jsDriver, output, events);
}
return this;
}
public JavascriptTestExecutor errorResponse(JavascriptStateValidator validator) {
Object output = jsExecutor.executeScript("return \"Error: \" + getParameterByName(\"error\") + \"\\n\" + \"Error description: \" + getParameterByName(\"error_description\")");
validator.validate(jsDriver, output, events);
return this;
}
public JavascriptTestExecutor wait(long millis, JavascriptStateValidator validator) {
pause(millis);
if (validator != null) {
validator.validate(jsDriver, null, events);
}
return this;
}
public JavascriptTestExecutor validateOutputField(JavascriptStateValidator validator) {
validator.validate(jsDriver, output, events);
return this;
}
}