388 lines
14 KiB
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;
|
|
}
|
|
}
|