Fix OfflineServletAdapterTest failures, and improve logging (#25724)

Closes #25714
Closes #14448

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-01-09 14:59:20 +01:00 committed by GitHub
parent 6ea9df2cf2
commit 03372d2f41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 50 deletions

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.adapter.page;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
@ -46,47 +47,43 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl
public AccessToken getAccessToken() { public AccessToken getAccessToken() {
try { try {
WaitUtils.waitUntilElement(accessToken).is().visible();
return JsonSerialization.readValue(accessToken.getText(), AccessToken.class); return JsonSerialization.readValue(accessToken.getText(), AccessToken.class);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); throw new RuntimeException(e);
} catch (NoSuchElementException nsee) { } catch (NoSuchElementException nsee) {
log.warn("No accessToken element found on the page"); throw LogScreenContents.fail(driver, "No accessToken element found on the page", nsee);
} }
return null;
} }
public RefreshToken getRefreshToken() { public RefreshToken getRefreshToken() {
try { try {
WaitUtils.waitUntilElement(refreshToken).is().visible();
return JsonSerialization.readValue(refreshToken.getText(), RefreshToken.class); return JsonSerialization.readValue(refreshToken.getText(), RefreshToken.class);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); throw new RuntimeException(e);
} catch (NoSuchElementException nsee) { } catch (NoSuchElementException nsee) {
log.warn("No refreshToken element found on the page"); throw LogScreenContents.fail(driver, "No refreshToken element found on the page", nsee);
} }
return null;
} }
public String getAccessTokenString() { public String getAccessTokenString() {
try { try {
WaitUtils.waitUntilElement(accessTokenString).is().visible();
return accessTokenString.getText(); return accessTokenString.getText();
} catch (NoSuchElementException nsee) { } catch (NoSuchElementException nsee) {
log.warn("No accessTokenString element found on the page"); throw LogScreenContents.fail(driver, "No accessTokenString element found on the page", nsee);
} }
return null;
} }
public String getRefreshTokenString() { public String getRefreshTokenString() {
try { try {
WaitUtils.waitUntilElement(refreshTokenString).is().visible();
return refreshTokenString.getText(); return refreshTokenString.getText();
} catch (NoSuchElementException nsee) { } catch (NoSuchElementException nsee) {
log.warn("No refreshTokenString element found on the page"); throw LogScreenContents.fail(driver, "No refreshTokenString element found on the page", nsee);
} }
return null;
} }
} }

View file

@ -0,0 +1,41 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.adapter.page;
import org.jboss.logging.Logger;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
/**
* @author Alexander Schwartz
*/
public class LogScreenContents {
private static final Logger log = Logger.getLogger(LogScreenContents.class);
public static <T extends Throwable> T fail(WebDriver webDriver, String message, T t) {
String screenShotBase64 = null;
if (webDriver instanceof TakesScreenshot) {
screenShotBase64 = ((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BASE64);
}
log.error(message + ", url '" + webDriver.getCurrentUrl() + "', title: " + webDriver.getTitle() + ", source: " + webDriver.getPageSource() +
(screenShotBase64 != null ? ", screenshot: " + screenShotBase64 : ""));
return t;
}
}

View file

@ -36,6 +36,9 @@ import java.util.List;
import org.jboss.shrinkwrap.api.asset.UrlAsset; import org.jboss.shrinkwrap.api.asset.UrlAsset;
import org.junit.Assert; import org.junit.Assert;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
@ -231,7 +234,7 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
DroneUtils.getCurrentDriver().navigate().to(timeOffsetUri); DroneUtils.getCurrentDriver().navigate().to(timeOffsetUri);
waitForPageToLoad(); waitForPageToLoad();
String pageSource = DroneUtils.getCurrentDriver().getPageSource(); String pageSource = DroneUtils.getCurrentDriver().getPageSource();
log.info(pageSource); assertThat(pageSource, containsString("Offset set successfully"));
} }
} }

View file

@ -215,7 +215,7 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
@Deployment(name = SecurePortal.DEPLOYMENT_NAME) @Deployment(name = SecurePortal.DEPLOYMENT_NAME)
protected static WebArchive securePortal() { protected static WebArchive securePortal() {
return servletDeployment(SecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class); return servletDeployment(SecurePortal.DEPLOYMENT_NAME, AdapterActionsFilter.class, CallAuthenticatedServlet.class);
} }
@Deployment(name = SecurePortalRewriteRedirectUri.DEPLOYMENT_NAME) @Deployment(name = SecurePortalRewriteRedirectUri.DEPLOYMENT_NAME)
protected static WebArchive securePortalRewriteRedirectUri() { protected static WebArchive securePortalRewriteRedirectUri() {

View file

@ -96,7 +96,7 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes
@Deployment(name = SecurePortal.DEPLOYMENT_NAME) @Deployment(name = SecurePortal.DEPLOYMENT_NAME)
protected static WebArchive securePortal() { protected static WebArchive securePortal() {
return servletDeployment(SecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class); return servletDeployment(SecurePortal.DEPLOYMENT_NAME, AdapterActionsFilter.class, CallAuthenticatedServlet.class);
} }
@Deployment(name = TokenMinTTLPage.DEPLOYMENT_NAME) @Deployment(name = TokenMinTTLPage.DEPLOYMENT_NAME)
@ -136,7 +136,6 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password"); testRealmLoginPage.form().login("bburke@redhat.com", "password");
URLAssert.assertCurrentUrlStartsWith(tokenMinTTLPage.getInjectedUrl().toString()); URLAssert.assertCurrentUrlStartsWith(tokenMinTTLPage.getInjectedUrl().toString());
Assert.assertNull(tokenMinTTLPage.getAccessToken());
ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout(); ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout();

View file

@ -34,9 +34,9 @@ import org.openqa.selenium.By;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
@ -72,6 +72,11 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
private final String DEFAULT_PASSWORD = "password"; private final String DEFAULT_PASSWORD = "password";
private final String OFFLINE_CLIENT_ID = "offline-client"; private final String OFFLINE_CLIENT_ID = "offline-client";
/**
* URL in the deployment that doesn't require authentication and which therefore can be used to trigger activities in the actionfilter.
*/
private static final String UNSECURED_URL = "unsecured/foo";
@Deployment(name = OfflineToken.DEPLOYMENT_NAME) @Deployment(name = OfflineToken.DEPLOYMENT_NAME)
protected static WebArchive offlineClient() { protected static WebArchive offlineClient() {
return servletDeployment(OfflineToken.DEPLOYMENT_NAME, AdapterActionsFilter.class, AbstractShowTokensServlet.class, OfflineTokenServlet.class, ErrorServlet.class, ServletTestUtils.class); return servletDeployment(OfflineToken.DEPLOYMENT_NAME, AdapterActionsFilter.class, AbstractShowTokensServlet.class, OfflineTokenServlet.class, ErrorServlet.class, ServletTestUtils.class);
@ -114,7 +119,6 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
assertCurrentUrlStartsWith(offlineTokenPage); assertCurrentUrlStartsWith(offlineTokenPage);
RefreshToken refreshToken = offlineTokenPage.getRefreshToken(); RefreshToken refreshToken = offlineTokenPage.getRefreshToken();
assertThat(refreshToken, notNullValue());
assertThat(TokenUtil.TOKEN_TYPE_OFFLINE, is(refreshToken.getType())); assertThat(TokenUtil.TOKEN_TYPE_OFFLINE, is(refreshToken.getType()));
assertThat(refreshToken.getExp(), nullValue()); assertThat(refreshToken.getExp(), nullValue());
@ -151,7 +155,7 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
loginPage.assertCurrent(); loginPage.assertCurrent();
} finally { } finally {
events.clear(); events.clear();
resetTimeOffsetAuthenticated(); resetTimeOffset();
} }
} }
@ -170,7 +174,6 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
assertCurrentUrlStartsWith(offlineTokenPage); assertCurrentUrlStartsWith(offlineTokenPage);
final RefreshToken refreshToken = offlineTokenPage.getRefreshToken(); final RefreshToken refreshToken = offlineTokenPage.getRefreshToken();
assertThat(refreshToken, notNullValue());
assertThat(refreshToken.getType(), is(TokenUtil.TOKEN_TYPE_OFFLINE)); assertThat(refreshToken.getType(), is(TokenUtil.TOKEN_TYPE_OFFLINE));
// Assert refresh works with increased time // Assert refresh works with increased time
@ -200,7 +203,7 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
loginPage.assertCurrent(); loginPage.assertCurrent();
} finally { } finally {
events.clear(); events.clear();
resetTimeOffsetAuthenticated(); resetTimeOffset();
} }
} }
@ -235,7 +238,6 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
assertCurrentUrlStartsWith(offlineTokenPage); assertCurrentUrlStartsWith(offlineTokenPage);
RefreshToken refreshToken = offlineTokenPage.getRefreshToken(); RefreshToken refreshToken = offlineTokenPage.getRefreshToken();
assertThat(refreshToken, notNullValue());
assertThat(refreshToken.getType(), is(TokenUtil.TOKEN_TYPE_OFFLINE)); assertThat(refreshToken.getType(), is(TokenUtil.TOKEN_TYPE_OFFLINE));
// Check that the client scopes have been granted by the user // Check that the client scopes have been granted by the user
@ -247,39 +249,31 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
AccountHelper.logout(adminClient.realm(TEST), DEFAULT_USERNAME); AccountHelper.logout(adminClient.realm(TEST), DEFAULT_USERNAME);
} finally { } finally {
events.clear(); events.clear();
resetTimeOffsetAuthenticated(); resetTimeOffset();
} }
} }
private void setAdapterAndServerTimeOffset(int timeOffset) { private void setAdapterAndServerTimeOffset(int timeOffset) {
super.setAdapterAndServerTimeOffset(timeOffset, offlineTokenPage.toString()); super.setAdapterAndServerTimeOffset(timeOffset, offlineTokenPage.toString() + UNSECURED_URL);
} }
private void resetTimeOffsetAuthenticated() { @Override
resetTimeOffsetAuthenticated(DEFAULT_USERNAME, DEFAULT_PASSWORD); public void resetTimeOffset() {
setAdapterServletTimeOffset(0, offlineTokenPage.toString() + UNSECURED_URL);
super.resetTimeOffset();
} }
/**
* Reset time offset for remote environment.
* After the token expiration, process of re-authentication is necessary.
*
* @param username
* @param password
*/
private void resetTimeOffsetAuthenticated(String username, String password) {
if (testContext.getAppServerInfo().isUndertow()) {
setAdapterAndServerTimeOffset(0);
return;
}
super.setAdapterServletTimeOffset(0, offlineTokenPage.toString());
if (loginPage.isCurrent()) { @Override
loginPage.login(username, password); protected void afterAbstractKeycloakTestRealmImport() {
waitForPageToLoad(); // after each re-import, ensure that the information stored in JWKPublicKeyLocator is reset
AccountHelper.logout(adminClient.realm(TEST), DEFAULT_USERNAME); String resetDeploymentUri = UriBuilder.fromUri(offlineTokenPage.toString() + UNSECURED_URL)
} .queryParam(AdapterActionsFilter.RESET_DEPLOYMENT_PARAM, "true")
setTimeOffset(0); .build().toString();
// Improve stability of the cleanup and have more time for synchronizing auth and app server living in different JVMs driver.navigate().to(resetDeploymentUri);
pause(400); waitForPageToLoad();
assertThat(driver.getPageSource(), containsString("Restarted PublicKeyLocator"));
super.afterAbstractKeycloakTestRealmImport();
} }
} }

View file

@ -53,6 +53,13 @@
<url-pattern>/error.html</url-pattern> <url-pattern>/error.html</url-pattern>
</servlet-mapping> </servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Unsecured</web-resource-name>
<url-pattern>/unsecured/*</url-pattern>
</web-resource-collection>
<!-- No auth-constraint = everybody has access -->
</security-constraint>
<security-constraint> <security-constraint>
<web-resource-collection> <web-resource-collection>
<web-resource-name>Users</web-resource-name> <web-resource-name>Users</web-resource-name>

View file

@ -23,11 +23,21 @@
<module-name>secure-portal</module-name> <module-name>secure-portal</module-name>
<filter>
<filter-name>AdapterActionsFilter</filter-name>
<filter-class>org.keycloak.testsuite.adapter.filter.AdapterActionsFilter</filter-class>
</filter>
<servlet> <servlet>
<servlet-name>Servlet</servlet-name> <servlet-name>Servlet</servlet-name>
<servlet-class>org.keycloak.testsuite.adapter.servlet.CallAuthenticatedServlet</servlet-class> <servlet-class>org.keycloak.testsuite.adapter.servlet.CallAuthenticatedServlet</servlet-class>
</servlet> </servlet>
<filter-mapping>
<filter-name>AdapterActionsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet-mapping> <servlet-mapping>
<servlet-name>Servlet</servlet-name> <servlet-name>Servlet</servlet-name>
<url-pattern>/*</url-pattern> <url-pattern>/*</url-pattern>