diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java index 80ed20f2cf..f54e15b529 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.cluster; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import org.jboss.arquillian.container.test.api.ContainerController; @@ -10,6 +11,7 @@ import static org.junit.Assert.assertTrue; import org.junit.Before; import org.keycloak.admin.client.Keycloak; import org.keycloak.models.Constants; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.arquillian.ContainerInfo; import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN; @@ -38,7 +40,7 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest { if (currentFailNodeIndex >= getClusterSize()) { currentFailNodeIndex = 0; } - logCurrentState(); + logFailoverSetup(); } protected ContainerInfo getCurrentFailNode() { @@ -51,7 +53,8 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest { return survivors; } - protected void logCurrentState() { + protected void logFailoverSetup() { + log.info("Current failover setup"); boolean started = controller.isStarted(getCurrentFailNode().getQualifier()); log.info("Fail node: " + getCurrentFailNode() + (started ? "" : " (stopped)")); for (ContainerInfo survivor : getCurrentSurvivorNodes()) { @@ -72,6 +75,10 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest { } } + protected ContainerInfo frontendNode() { + return suiteContext.getAuthServerInfo(); + } + protected ContainerInfo backendNode(int i) { return suiteContext.getAuthServerBackendsInfo().get(i); } @@ -101,15 +108,22 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest { controller.kill(node.getQualifier()); } - protected Keycloak getAdminClientFor(ContainerInfo backendNode) { - return backendAdminClients.get(backendNode); + protected Keycloak getAdminClientFor(ContainerInfo node) { + return node.equals(suiteContext.getAuthServerInfo()) + ? adminClient // frontend client + : backendAdminClients.get(node); } @Before public void beforeClusterTest() { failback(); - logCurrentState(); + logFailoverSetup(); pause(3000); } + @Override + public void addTestRealms(List testRealms) { + // no test realms will be created by the default + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java new file mode 100644 index 0000000000..c129bbe4d0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java @@ -0,0 +1,148 @@ +package org.keycloak.testsuite.cluster; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.ReflectionToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import static org.junit.Assert.assertFalse; +import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +/** + * + * @author tkyjovsk + */ +public abstract class AbstractInvalidationClusterTest extends AbstractClusterTest { + + private final SecureRandom random = new SecureRandom(); + + protected String randomString(int length) { + return new BigInteger(130, random).toString(length); + } + + protected RealmRepresentation createTestRealmRepresentation() { + RealmRepresentation testRealm = new RealmRepresentation(); + testRealm.setRealm("test_" + randomString(5)); + testRealm.setEnabled(true); + return testRealm; + } + + protected abstract T createTestEntityRepresentation(); + + @Test + public void crudWithoutFailover() { + crud(false); + } + + @Test + public void crudWithFailover() { + crud(true); + } + + public void crud(boolean backendFailover) { + T testEntity = createTestEntityRepresentation(); + + // CREATE + testEntity = createEntityOnCurrentFailNode(testEntity); + + if (backendFailover) { + failure(); + } + + assertEntityOnSurvivorNodesEqualsTo(testEntity); + + failback(); + iterateCurrentFailNode(); + + // UPDATE(s) + testEntity = testEntityUpdates(testEntity, backendFailover); + + // DELETE + deleteEntityOnCurrentFailNode(testEntity); + + if (backendFailover) { + failure(); + } + + assertEntityOnSurvivorNodesIsDeleted(testEntity); + } + + protected abstract T createEntity(T testEntity, ContainerInfo node); + + protected abstract T readEntity(T entity, ContainerInfo node); + + protected abstract T updateEntity(T entity, ContainerInfo node); + + protected abstract void deleteEntity(T testEntity, ContainerInfo node); + + protected T createEntityOnCurrentFailNode(T testEntity) { + return createEntity(testEntity, getCurrentFailNode()); + } + + protected T readEntityOnCurrentFailNode(T entity) { + return readEntity(entity, getCurrentFailNode()); + } + + protected T updateEntityOnCurrentFailNode(T entity) { + return updateEntity(entity, getCurrentFailNode()); + } + + protected void deleteEntityOnCurrentFailNode(T testEntity) { + deleteEntity(testEntity, getCurrentFailNode()); + } + + protected abstract T testEntityUpdates(T testEntity, boolean backendFailover); + + protected void verifyEntityUpdateDuringFailover(T testEntity, boolean backendFailover) { + if (backendFailover) { + failure(); + } + + assertEntityOnSurvivorNodesEqualsTo(testEntity); + + failback(); + iterateCurrentFailNode(); + } + + protected List excludedComparisonFields = new ArrayList<>(); + + protected void assertEntityOnSurvivorNodesEqualsTo(T testEntityOnFailNode) { + boolean entityDiffers = false; + for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) { + T testEntityOnSurvivorNode = readEntity(testEntityOnFailNode, survivorNode); + if (EqualsBuilder.reflectionEquals(testEntityOnSurvivorNode, testEntityOnFailNode, excludedComparisonFields)) { + log.info("Verification on survivor " + survivorNode + " PASSED"); + } else { + entityDiffers = true; + log.error("Verification on survivor " + survivorNode + " FAILED"); + String tf = ReflectionToStringBuilder.reflectionToString(testEntityOnFailNode, ToStringStyle.SHORT_PREFIX_STYLE); + String ts = ReflectionToStringBuilder.reflectionToString(testEntityOnSurvivorNode, ToStringStyle.SHORT_PREFIX_STYLE); + log.error("\nEntity on fail node: \n\n" + tf + "\n" + + "\nEntity on survivor node: \n" + ts + "\n" + + "\nDifference: \n" + StringUtils.difference(tf, ts) + "\n"); + } + } + assertFalse(entityDiffers); + } + + private void assertEntityOnSurvivorNodesIsDeleted(T testEntityOnFailNode) { + // check if deleted from all survivor nodes + boolean entityExists = false; + for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) { + T testEntityOnSurvivorNode = readEntity(testEntityOnFailNode, survivorNode); + if (testEntityOnSurvivorNode == null) { + log.info("Verification of deletion on survivor " + survivorNode + " PASSED"); + } else { + entityExists = true; + log.error("Verification of deletion on survivor " + survivorNode + " FAILED"); + } + } + assertFalse(entityExists); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTestWithTestRealm.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTestWithTestRealm.java new file mode 100644 index 0000000000..a2e2013898 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTestWithTestRealm.java @@ -0,0 +1,26 @@ +package org.keycloak.testsuite.cluster; + +import org.junit.Before; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +/** + * + * @author tkyjovsk + */ +public abstract class AbstractInvalidationClusterTestWithTestRealm extends AbstractInvalidationClusterTest { + + protected String testRealmName = null; + + @Before + public void createTestRealm() { + createTestRealm(frontendNode()); + } + + protected void createTestRealm(ContainerInfo node) { + RealmRepresentation r = createTestRealmRepresentation(); + getAdminClientFor(node).realms().create(r); + testRealmName = r.getRealm(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientInvalidationClusterTest.java new file mode 100644 index 0000000000..f654260fdd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientInvalidationClusterTest.java @@ -0,0 +1,84 @@ +package org.keycloak.testsuite.cluster; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import static org.junit.Assert.assertNull; +import org.junit.Before; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +/** + * + * @author tkyjovsk + */ +public class ClientInvalidationClusterTest extends AbstractInvalidationClusterTestWithTestRealm { + + @Before + public void setExcludedComparisonFields() { + excludedComparisonFields.add("protocolMappers"); + } + + @Override + protected ClientRepresentation createTestEntityRepresentation() { + ClientRepresentation client = new ClientRepresentation(); + String s = randomString(5); + client.setClientId("client_" + s); + client.setName("name_" + s); + return client; + } + + protected ClientsResource clients(ContainerInfo node) { + return getAdminClientFor(node).realm(testRealmName).clients(); + } + + @Override + protected ClientRepresentation createEntity(ClientRepresentation client, ContainerInfo node) { + Response response = clients(node).create(client); + String id = ApiUtil.getCreatedId(response); + response.close(); + client.setId(id); + return readEntity(client, node); + } + + @Override + protected ClientRepresentation readEntity(ClientRepresentation client, ContainerInfo node) { + ClientRepresentation u = null; + try { + u = clients(node).get(client.getId()).toRepresentation(); + } catch (NotFoundException nfe) { + // exoected when client doesn't exist + } + return u; + } + + @Override + protected ClientRepresentation updateEntity(ClientRepresentation client, ContainerInfo node) { + clients(node).get(client.getId()).update(client); + return readEntity(client, node); + } + + @Override + protected void deleteEntity(ClientRepresentation client, ContainerInfo node) { + clients(node).get(client.getId()).remove(); + assertNull(readEntity(client, node)); + } + + @Override + protected ClientRepresentation testEntityUpdates(ClientRepresentation client, boolean backendFailover) { + + // clientId + client.setClientId(client.getClientId() + "_updated"); + client = updateEntity(client, getCurrentFailNode()); + verifyEntityUpdateDuringFailover(client, backendFailover); + + // name + client.setName(client.getName() + "_updated"); + client = updateEntity(client, getCurrentFailNode()); + verifyEntityUpdateDuringFailover(client, backendFailover); + + return client; + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/EntityInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/EntityInvalidationClusterTest.java deleted file mode 100644 index 74e6d1f334..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/EntityInvalidationClusterTest.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.keycloak.testsuite.cluster; - -import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import org.junit.Test; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.testsuite.arquillian.ContainerInfo; -import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; - -/** - * - * @author tkyjovsk - */ -public class EntityInvalidationClusterTest extends AbstractClusterTest { - - @Override - public void addTestRealms(List testRealms) { - } - - @Test - public void createRealmViaFrontend() { - String realm = TEST + "_fe"; - - RealmRepresentation testRealm = new RealmRepresentation(); - testRealm.setRealm(realm); - testRealm.setEnabled(true); - - // CREATE on frontend - adminClient.realms().create(testRealm); - - // check if created on frontend - RealmRepresentation testRealmOnFrontend = adminClient.realms().realm(realm).toRepresentation(); - assertEquals(testRealmOnFrontend.getRealm(), testRealm.getRealm()); - - // check if created on backend nodes - for (ContainerInfo backend : suiteContext.getAuthServerBackendsInfo()) { - RealmRepresentation testRealmOnBackend = getAdminClientFor(backend).realms().realm(realm).toRepresentation(); - assertEquals(testRealmOnBackend.getId(), testRealmOnFrontend.getId()); - assertEquals(testRealmOnBackend.getRealm(), testRealmOnFrontend.getRealm()); - } - } - - @Test - public void realmCRUDWithoutFailover() { - realmCRUD(TEST + "_wofo", false); - } - - @Test - public void realmCRUDWithFailover() { - realmCRUD(TEST + "_wfo", true); - } - - public void realmCRUD(String realmName, boolean backendFailover) { - - // CREATE on current fail node - log.info("Creating realm on : " + getCurrentFailNode()); - RealmRepresentation testRealm = new RealmRepresentation(); - testRealm.setRealm(realmName); - testRealm.setEnabled(true); - getAdminClientFor(getCurrentFailNode()).realms().create(testRealm); - - // check if created on fail node - RealmRepresentation testRealmOnFailNode = getAdminClientFor(getCurrentFailNode()).realms().realm(realmName).toRepresentation(); - assertEquals(testRealmOnFailNode.getRealm(), testRealm.getRealm()); - - if (backendFailover) { - failure(); - } - - // check if created on survivor nodes - for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) { - RealmRepresentation testRealmOnSurvivorNode = getAdminClientFor(survivorNode).realms().realm(realmName).toRepresentation(); - assertEquals(testRealmOnFailNode.getId(), testRealmOnSurvivorNode.getId()); - assertEquals(testRealmOnFailNode.getRealm(), testRealmOnSurvivorNode.getRealm()); - log.info("Creation on survivor: " + survivorNode + " verified"); - } - - failback(); - iterateCurrentFailNode(); - - // UPDATE on current fail node - log.info("Updating realm on: " + getCurrentFailNode()); - String realmBeforeUpdate = realmName; - realmName += "_updated"; - testRealm = testRealmOnFailNode; - testRealm.setRealm(realmName); - getAdminClientFor(getCurrentFailNode()).realms().realm(realmBeforeUpdate).update(testRealm); - - // check if updated on fail node - testRealmOnFailNode = getAdminClientFor(getCurrentFailNode()).realms().realm(realmName).toRepresentation(); - assertEquals(testRealmOnFailNode.getRealm(), testRealm.getRealm()); - - if (backendFailover) { - failure(); - } - - // check if updated on survivor nodes - for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) { - RealmRepresentation testRealmOnSurvivorNode = getAdminClientFor(survivorNode).realms().realm(realmName).toRepresentation(); - assertEquals(testRealmOnFailNode.getId(), testRealmOnSurvivorNode.getId()); - assertEquals(testRealmOnFailNode.getRealm(), testRealmOnSurvivorNode.getRealm()); - log.info("Update on survivor: " + survivorNode + " verified"); - } - - failback(); - iterateCurrentFailNode(); - - // DELETE on current fail node - log.info("Deleting realm on: " + getCurrentFailNode()); - getAdminClientFor(getCurrentFailNode()).realms().realm(realmName).remove(); - - if (backendFailover) { - failure(); - } - - // check if deleted from all survivor nodes - boolean realmStillExists = false; - for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) { - boolean realmStillExistsOnSurvivor = false; - for (RealmRepresentation realmOnSurvivorNode : getAdminClientFor(survivorNode).realms().findAll()) { - if (realmName.equals(realmOnSurvivorNode.getRealm()) - || testRealmOnFailNode.getId().equals(realmOnSurvivorNode.getId())) { - realmStillExistsOnSurvivor = true; - realmStillExists = true; - break; - } - } - log.error("Deletion on survivor: " + survivorNode + (realmStillExistsOnSurvivor ? " FAILED" : " verified")); - } - assertFalse(realmStillExists); - } - -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/RealmInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/RealmInvalidationClusterTest.java new file mode 100644 index 0000000000..af66a3e823 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/RealmInvalidationClusterTest.java @@ -0,0 +1,79 @@ +package org.keycloak.testsuite.cluster; + +import javax.ws.rs.NotFoundException; +import static org.junit.Assert.assertNull; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +/** + * + * @author tkyjovsk + */ +public class RealmInvalidationClusterTest extends AbstractInvalidationClusterTest { + + @Override + protected RealmRepresentation createTestEntityRepresentation() { + return createTestRealmRepresentation(); + } + + @Override + protected RealmRepresentation createEntity(RealmRepresentation realm, ContainerInfo node) { + log.info("Creating realm on : " + getCurrentFailNode()); + getAdminClientFor(getCurrentFailNode()).realms().create(realm); + // get created entity + return readEntity(realm, node); + } + + @Override + protected RealmRepresentation readEntity(RealmRepresentation realm, ContainerInfo node) { + RealmRepresentation realmOnNode = null; + try { + realmOnNode = getAdminClientFor(node).realm(realm.getRealm()).toRepresentation(); + } catch (NotFoundException nfe) { + // expected if realm not found + } + return realmOnNode; + } + + @Override + protected RealmRepresentation updateEntity(RealmRepresentation realm, ContainerInfo node) { + getAdminClientFor(node).realms().realm(realm.getRealm()).update(realm); + return readEntity(realm, node); + } + + @Override + protected void deleteEntity(RealmRepresentation realm, ContainerInfo node) { + log.info("Deleting realm on: " + getCurrentFailNode()); + getAdminClientFor(node).realms().realm(realm.getRealm()).remove(); + // check if deleted + assertNull(readEntity(realm, node)); + } + + @Override + protected RealmRepresentation testEntityUpdates(RealmRepresentation realm, boolean backendFailover) { + + realm = updateRealmName(realm, realm.getRealm() + "_updated"); + verifyEntityUpdateDuringFailover(realm, backendFailover); + + realm = updateRealmEnabled(realm); + verifyEntityUpdateDuringFailover(realm, backendFailover); + + return realm; + } + + protected RealmRepresentation updateRealmName(RealmRepresentation realm, String newName) { + log.info("Updating realm on: " + getCurrentFailNode()); + String originalName = realm.getRealm(); + realm.setRealm(newName); + + getAdminClientFor(getCurrentFailNode()).realms().realm(originalName).update(realm); + return readEntity(realm, getCurrentFailNode()); + } + + protected RealmRepresentation updateRealmEnabled(RealmRepresentation realm) { + log.info("Updating realm on: " + getCurrentFailNode()); + realm.setEnabled(!realm.isEnabled()); + return updateEntity(realm, getCurrentFailNode()); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/UserInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/UserInvalidationClusterTest.java new file mode 100644 index 0000000000..3df64987d8 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/UserInvalidationClusterTest.java @@ -0,0 +1,82 @@ +package org.keycloak.testsuite.cluster; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import static org.junit.Assert.assertNull; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +/** + * + * @author tkyjovsk + */ +public class UserInvalidationClusterTest extends AbstractInvalidationClusterTestWithTestRealm { + + @Override + protected UserRepresentation createTestEntityRepresentation() { + String firstName = "user"; + String lastName = randomString(5); + UserRepresentation user = new UserRepresentation(); + user.setUsername(firstName + "_" + lastName); + user.setEmail(user.getUsername() + "@email.test"); + user.setFirstName(firstName); + user.setLastName(lastName); + return user; + } + + protected UsersResource users(ContainerInfo node) { + return getAdminClientFor(node).realm(testRealmName).users(); + } + + @Override + protected UserRepresentation createEntity(UserRepresentation user, ContainerInfo node) { + Response response = users(node).create(user); + String id = ApiUtil.getCreatedId(response); + response.close(); + user.setId(id); + return readEntity(user, node); + } + + @Override + protected UserRepresentation readEntity(UserRepresentation user, ContainerInfo node) { + UserRepresentation u = null; + try { + u = users(node).get(user.getId()).toRepresentation(); + } catch (NotFoundException nfe) { + // exoected when user doesn't exist + } + return u; + } + + @Override + protected UserRepresentation updateEntity(UserRepresentation user, ContainerInfo node) { + users(node).get(user.getId()).update(user); + return readEntity(user, node); + } + + @Override + protected void deleteEntity(UserRepresentation user, ContainerInfo node) { + users(node).get(user.getId()).remove(); + assertNull(readEntity(user, node)); + } + + @Override + protected UserRepresentation testEntityUpdates(UserRepresentation user, boolean backendFailover) { + + // username + user.setUsername(user.getUsername() + "_updated"); + user = updateEntity(user, getCurrentFailNode()); + verifyEntityUpdateDuringFailover(user, backendFailover); + + // first+lastName + user.setFirstName(user.getFirstName() + "_updated"); + user.setLastName(user.getLastName() + "_updated"); + user = updateEntity(user, getCurrentFailNode()); + verifyEntityUpdateDuringFailover(user, backendFailover); + + return user; + } + +}