/* * Copyright 2016 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; import org.hamcrest.CoreMatchers; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.junit.Assert; import org.junit.rules.TestRule; import org.junit.runners.model.Statement; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.util.TokenUtil; import javax.ws.rs.core.Response; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.is; /** * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> */ public class AssertEvents implements TestRule { public static final String DEFAULT_CLIENT_ID = "test-app"; public static final String DEFAULT_IP_ADDRESS = "127.0.0.1"; public static final String DEFAULT_REALM = "test"; public static final String DEFAULT_USERNAME = "test-user@localhost"; String defaultRedirectUri = "http://localhost:8180/auth/realms/master/app/auth"; private AbstractKeycloakTest context; public AssertEvents(AbstractKeycloakTest ctx) { context = ctx; } @Override public Statement apply(final Statement base, org.junit.runner.Description description) { return new Statement() { @Override public void evaluate() throws Throwable { // TODO: Ideally clear the queue just before testClass rather then before each method clear(); base.evaluate(); // TODO Test should fail if there are leftover events } }; } public EventRepresentation poll() { EventRepresentation event = fetchNextEvent(); Assert.assertNotNull("Event expected", event); return event; } public void assertEmpty() { EventRepresentation event = fetchNextEvent(); Assert.assertNull("Empty event queue expected, but there is " + event, event); } public void clear() { context.getTestingClient().testing().clearEventQueue(); } public ExpectedEvent expectRequiredAction(EventType event) { return expectLogin().event(event).removeDetail(Details.CONSENT).session(Matchers.isEmptyOrNullString()); } public ExpectedEvent expectLogin() { return expect(EventType.LOGIN) .detail(Details.CODE_ID, isCodeId()) //.detail(Details.USERNAME, DEFAULT_USERNAME) //.detail(Details.AUTH_METHOD, OIDCLoginProtocol.LOGIN_PROTOCOL) //.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE) .detail(Details.REDIRECT_URI, defaultRedirectUri) .detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED) .session(isUUID()); } public ExpectedEvent expectClientLogin() { return expect(EventType.CLIENT_LOGIN) .detail(Details.CODE_ID, isCodeId()) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .detail(Details.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS) .removeDetail(Details.CODE_ID) .session(isUUID()); } public ExpectedEvent expectSocialLogin() { return expect(EventType.LOGIN) .detail(Details.CODE_ID, isCodeId()) .detail(Details.USERNAME, DEFAULT_USERNAME) .detail(Details.AUTH_METHOD, "form") .detail(Details.REDIRECT_URI, defaultRedirectUri) .session(isUUID()); } public ExpectedEvent expectCodeToToken(String codeId, String sessionId) { return expect(EventType.CODE_TO_TOKEN) .detail(Details.CODE_ID, codeId) .detail(Details.TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .session(sessionId); } public ExpectedEvent expectRefresh(String refreshTokenId, String sessionId) { return expect(EventType.REFRESH_TOKEN) .detail(Details.TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_ID, refreshTokenId) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH) .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID()) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .session(sessionId); } public ExpectedEvent expectLogout(String sessionId) { return expect(EventType.LOGOUT).client((String) null) .detail(Details.REDIRECT_URI, defaultRedirectUri) .session(sessionId); } public ExpectedEvent expectRegister(String username, String email) { UserRepresentation user = username != null ? getUser(username) : null; return expect(EventType.REGISTER) .user(user != null ? user.getId() : null) .detail(Details.USERNAME, username) .detail(Details.EMAIL, email) .detail(Details.REGISTER_METHOD, "form") .detail(Details.REDIRECT_URI, defaultRedirectUri); } public ExpectedEvent expectAccount(EventType event) { return expect(event).client("account"); } public ExpectedEvent expect(EventType event) { return new ExpectedEvent() .realm(defaultRealmId()) .client(DEFAULT_CLIENT_ID) .user(defaultUserId()) .ipAddress(DEFAULT_IP_ADDRESS) .session((String) null) .event(event); } public class ExpectedEvent { private EventRepresentation expected = new EventRepresentation(); private Matcher<String> realmId; private Matcher<String> userId; private Matcher<String> sessionId; private HashMap<String, Matcher<? super String>> details; public ExpectedEvent realm(Matcher<String> realmId) { this.realmId = realmId; return this; } public ExpectedEvent realm(RealmRepresentation realm) { return realm(CoreMatchers.equalTo(realm.getId())); } public ExpectedEvent realm(String realmId) { return realm(CoreMatchers.equalTo(realmId)); } public ExpectedEvent client(ClientRepresentation client) { expected.setClientId(client.getClientId()); return this; } public ExpectedEvent client(String clientId) { expected.setClientId(clientId); return this; } public ExpectedEvent user(UserRepresentation user) { return user(user.getId()); } public ExpectedEvent user(String userId) { return user(CoreMatchers.equalTo(userId)); } public ExpectedEvent user(Matcher<String> userId) { this.userId = userId; return this; } public ExpectedEvent session(UserSessionRepresentation session) { return session(session.getId()); } public ExpectedEvent session(String sessionId) { return session(CoreMatchers.equalTo(sessionId)); } public ExpectedEvent session(Matcher<String> sessionId) { this.sessionId = sessionId; return this; } public ExpectedEvent ipAddress(String ipAddress) { expected.setIpAddress(ipAddress); return this; } public ExpectedEvent event(EventType e) { expected.setType(e.name()); return this; } public ExpectedEvent detail(String key, String value) { return detail(key, CoreMatchers.equalTo(value)); } public ExpectedEvent detail(String key, Matcher<? super String> matcher) { if (details == null) { details = new HashMap<String, Matcher<? super String>>(); } details.put(key, matcher); return this; } public ExpectedEvent removeDetail(String key) { if (details != null) { details.remove(key); } return this; } public ExpectedEvent clearDetails() { if (details != null) details.clear(); return this; } public ExpectedEvent error(String error) { expected.setError(error); return this; } public EventRepresentation assertEvent() { return assertEvent(poll()); } public EventRepresentation assertEvent(EventRepresentation actual) { if (expected.getError() != null && ! expected.getType().toString().endsWith("_ERROR")) { expected.setType(expected.getType() + "_ERROR"); } Assert.assertThat("type", actual.getType(), is(expected.getType())); Assert.assertThat("realm ID", actual.getRealmId(), is(realmId)); Assert.assertThat("client ID", actual.getClientId(), is(expected.getClientId())); Assert.assertThat("error", actual.getError(), is(expected.getError())); Assert.assertThat("ip address", actual.getIpAddress(), is(expected.getIpAddress())); Assert.assertThat("user ID", actual.getUserId(), is(userId)); Assert.assertThat("session ID", actual.getSessionId(), is(sessionId)); if (details == null || details.isEmpty()) { // Assert.assertNull(actual.getDetails()); } else { Assert.assertNotNull(actual.getDetails()); for (Map.Entry<String, Matcher<? super String>> d : details.entrySet()) { String actualValue = actual.getDetails().get(d.getKey()); if (!actual.getDetails().containsKey(d.getKey())) { Assert.fail(d.getKey() + " missing"); } Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, is(d.getValue())); } /* for (String k : actual.getDetails().keySet()) { if (!details.containsKey(k)) { Assert.fail(k + " was not expected"); } } */ } return actual; } } public static Matcher<String> isCodeId() { return isUUID(); } public static Matcher<String> isUUID() { return new TypeSafeMatcher<String>() { @Override protected boolean matchesSafely(String item) { return 36 == item.length() && item.charAt(8) == '-' && item.charAt(13) == '-' && item.charAt(18) == '-' && item.charAt(23) == '-'; } @Override public void describeTo(Description description) { description.appendText("Not an UUID"); } }; } public Matcher<String> defaultRealmId() { return new TypeSafeMatcher<String>() { private String realmId; @Override protected boolean matchesSafely(String item) { return item.equals(getRealmId()); } @Override public void describeTo(Description description) { description.appendText(getRealmId()); } private String getRealmId() { if (realmId == null) { RealmRepresentation realm = context.adminClient.realm(DEFAULT_REALM).toRepresentation(); if (realm == null) { throw new RuntimeException("Default user does not exist: " + DEFAULT_USERNAME + ". Make sure to add it to your test realm."); } realmId = realm.getId(); } return realmId; } }; } public Matcher<String> defaultUserId() { return new TypeSafeMatcher<String>() { private String userId; @Override protected boolean matchesSafely(String item) { return item.equals(getUserId()); } @Override public void describeTo(Description description) { description.appendText(getUserId()); } private String getUserId() { if (userId == null) { UserRepresentation user = getUser(DEFAULT_USERNAME); if (user == null) { throw new RuntimeException("Default user does not exist: " + DEFAULT_USERNAME + ". Make sure to add it to your test realm."); } userId = user.getId(); } return userId; } }; } private UserRepresentation getUser(String username) { List<UserRepresentation> users = context.adminClient.realm(DEFAULT_REALM).users().search(username, null, null, null, 0, 1); return users.isEmpty() ? null : users.get(0); } private EventRepresentation fetchNextEvent() { return context.testingClient.testing().pollEvent(); } }