diff --git a/keycloak-upgrade/README.md b/keycloak-upgrade/README.md new file mode 100644 index 0000000000..33522f8a58 --- /dev/null +++ b/keycloak-upgrade/README.md @@ -0,0 +1,45 @@ +# Keycloak Upgrade Integration Tests + +This test verifies that data created with the previous Keycloak image still works after installing a newer `folio-keycloak` image. + +The test is split into two runners because the Keycloak container must be replaced between phases while preserving the Keycloak database. + +## Run + +When no state file exists, the seed phase generates a random tenant name and id, then writes them to `target/keycloak-upgrade-tenant.properties`. The verify phase reads the same file. + +If the seed phase fails after tenant creation, remove `target/keycloak-upgrade-tenant.properties` to generate a new tenant, or clean up the failed tenant before rerunning the seed phase. + +Start the environment with the old Keycloak image, then seed upgrade data: + +```shell +mvn clean test -pl common,testrail-integration,keycloak-upgrade \ + -DfailIfNoTests=false \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dtest=KeycloakUpgradeSeedTests \ + -DargLine="-Dkarate.env=eureka2" +``` + +Restart Keycloak with the upgraded image against the same database, wait for Keycloak and Kong to become ready, then verify: + +```shell +mvn test -pl common,testrail-integration,keycloak-upgrade \ + -DfailIfNoTests=false \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dtest=KeycloakUpgradeVerifyTests \ + -DargLine="-Dkarate.env=eureka2" +``` + +To use an explicit state file location, add `-Dkeycloak.upgrade.stateFile=/path/to/keycloak-upgrade-tenant.properties` to both `argLine` values. + +To use an explicit tenant instead of the generated one, add both `-DtestTenant=` and `-DtestTenantId=` to the seed command. The seed phase still writes them to the state file for the verify command. + +## What It Covers + +- Tenant authentication state survives the Keycloak restart. +- Existing FOLIO user and credentials still work. +- Login, refresh, user lookup, and effective permission resolution still work through FOLIO APIs. +- FOLIO-issued tokens still contain the expected `sub`, `user_id`, and client claims. +- Tenant metadata can still be updated through the FOLIO tenant manager API. +- Role/capability assignments created before the upgrade still resolve after the upgrade. +- The verify phase cleans up the generated tenant after a successful run. diff --git a/keycloak-upgrade/pom.xml b/keycloak-upgrade/pom.xml new file mode 100644 index 0000000000..d58333a43b --- /dev/null +++ b/keycloak-upgrade/pom.xml @@ -0,0 +1,42 @@ + + + + + folio-integration-tests + org.folio + 1.0-SNAPSHOT + + 4.0.0 + + keycloak-upgrade + 1.0-SNAPSHOT + jar + + + + org.folio + common + 1.0-SNAPSHOT + + + org.folio + testrail-integration + 1.0-SNAPSHOT + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + + diff --git a/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/destroy-upgrade-data.feature b/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/destroy-upgrade-data.feature new file mode 100644 index 0000000000..b453520fab --- /dev/null +++ b/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/destroy-upgrade-data.feature @@ -0,0 +1,8 @@ +Feature: destroy Keycloak upgrade test data + + Background: + * url baseUrl + * configure readTimeout = 3000000 + + Scenario: delete upgrade test tenant + * call read('classpath:common/eureka/destroy-data.feature') { testTenantId: '#(testTenantId)' } diff --git a/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/seed-before-upgrade.feature b/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/seed-before-upgrade.feature new file mode 100644 index 0000000000..db764d7102 --- /dev/null +++ b/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/seed-before-upgrade.feature @@ -0,0 +1,124 @@ +Feature: seed data before Keycloak upgrade + + Background: + * url baseUrl + * configure cookies = null + * configure retry = { count: 20, interval: 5000 } + + * table modules + | name | + | 'mod-users-keycloak' | + | 'mod-login-keycloak' | + | 'mod-roles-keycloak' | + + * table userPermissions + | name | + | 'login.item.post' | + | 'auth.token.post' | + | 'auth.refreshtoken.post' | + | 'auth.token.sign.post' | + | 'auth.token.refresh.post' | + | 'users.item.get' | + | 'users-keycloak.item.get' | + | 'roles.item.get' | + | 'roles.item.delete' | + | 'roles.item.post' | + | 'roles.collection.get' | + | 'roles.users.item.get' | + | 'roles.users.item.post' | + | 'roles.users.collection.get' | + | 'capabilities.item.get' | + | 'capabilities.collection.get' | + | 'role-capabilities.collection.post' | + | 'role-capabilities.collection.get' | + | 'permissions.users.item.get' | + + Scenario: create tenant, user credentials, and role data before upgrade + # Create the tenant and enable required applications before requesting tenant-scoped Keycloak tokens. + * call read('classpath:common/eureka/setup-users.feature@createTenant') + * call read('classpath:common/eureka/setup-users.feature@createEntitlement') + + # Wait until the tenant realm is visible before retrieving the tenant-scoped module token. + * def keycloakResponse = call read('classpath:common/eureka/keycloak.feature@getKeycloakMasterToken') + * def keycloakMasterToken = keycloakResponse.response.access_token + Given url baseKeycloakUrl + And path 'admin', 'realms', testTenant + And header Authorization = 'Bearer ' + keycloakMasterToken + And retry until responseStatus == 200 + When method get + Then status 200 + + # Create the test user, assign credentials, and grant the permissions needed for verification. + * configure headers = null + * configure cookies = null + * call read('classpath:common/eureka/setup-users.feature@getAuthorizationToken') + * call read('classpath:common/eureka/setup-users.feature@createTestUser') + * call read('classpath:common/eureka/setup-users.feature@specifyUserCredentials') + * call read('classpath:common/eureka/setup-users.feature@addUserCapabilities') + * def testUserId = karate.get('userId') + + # Verify the user can authenticate before the Keycloak upgrade. + Given url baseUrl + * configure cookies = null + Given path 'authn', 'login' + And headers { 'Content-Type': 'application/json', 'x-okapi-tenant': '#(testTenant)' } + And request { username: '#(testUser.name)', password: '#(testUser.password)' } + When method post + Then status 201 + And match response.okapiToken == '#string' + And match response.refreshToken == '#string' + And match responseCookies contains { folioAccessToken: '#present', folioRefreshToken: '#present' } + * def userToken = response.okapiToken + * def jwt = decodeJwtPayload(userToken) + And match jwt.sub == testUser.name + And match jwt.user_id == testUserId + + # Resolve a known capability that will be attached to a role before the upgrade. + * configure headers = { 'Content-Type': 'application/json', 'x-okapi-token': '#(userToken)', 'Accept': '*/*', 'x-okapi-tenant': '#(testTenant)' } + Given path 'capabilities' + And param query = 'permission=="' + upgradeRolePermission + '"' + When method get + Then status 200 + And match response.capabilities == '#[1]' + * def upgradeCapability = response.capabilities[0] + + # Create a durable role fixture that should survive the Keycloak upgrade. + Given path 'roles' + And request { name: '#(upgradeRoleName)', description: '#(upgradeRoleDescription)', type: 'REGULAR' } + When method post + Then status 201 + And match response.name == upgradeRoleName + * def upgradeRoleId = response.id + + # Attach the capability to the role so post-upgrade access resolution can be verified. + Given path 'roles', 'capabilities' + And request { roleId: '#(upgradeRoleId)', capabilityIds: ['#(upgradeCapability.id)'] } + When method post + Then status 201 + And match response.roleCapabilities[*].capabilityId contains upgradeCapability.id + + # Assign the role to the test user before the upgrade. + Given path 'roles', 'users' + And request { userId: '#(testUserId)', roleIds: ['#(upgradeRoleId)'] } + When method post + Then status 201 + And match response.userRoles[*].roleId contains upgradeRoleId + + # Confirm effective permissions include the role capability before the upgrade. + Given path 'permissions', 'users', testUserId + When method get + Then status 200 + And match response.permissions contains upgradeRolePermission + + # Verify module-to-module authentication can read the seeded user before the upgrade. + * configure headers = null + * configure cookies = null + * call read('classpath:common/eureka/setup-users.feature@getAuthorizationToken') + * def m2mToken = karate.get('accessToken') + Given url baseUrl + And path 'users-keycloak', 'users', testUserId + And headers { 'x-okapi-tenant': '#(testTenant)', 'x-okapi-token': '#(m2mToken)' } + When method get + Then status 200 + And match response.id == testUserId + And match response.username == testUser.name diff --git a/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/verify-after-upgrade.feature b/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/verify-after-upgrade.feature new file mode 100644 index 0000000000..25593ff88e --- /dev/null +++ b/keycloak-upgrade/src/main/resources/eureka/keycloak-upgrade/features/verify-after-upgrade.feature @@ -0,0 +1,118 @@ +Feature: verify data after Keycloak upgrade + + Background: + * url baseUrl + * configure cookies = null + * configure retry = { count: 20, interval: 5000 } + + Scenario: verify pre-upgrade tenant data through FOLIO APIs after restart + # Verify the pre-upgrade user can still authenticate through the FOLIO login API. + * configure cookies = null + Given path 'authn', 'login' + And headers { 'Content-Type': 'application/json', 'x-okapi-tenant': '#(testTenant)' } + And request { username: '#(testUser.name)', password: '#(testUser.password)' } + When method post + Then status 201 + And match response contains { okapiToken: '#string', refreshToken: '#string' } + And match responseCookies contains { folioAccessToken: '#present', folioRefreshToken: '#present' } + * def loginToken = response.okapiToken + * def refreshToken = responseCookies.folioRefreshToken.value + * def jwt = decodeJwtPayload(loginToken) + And match jwt.sub == testUser.name + And match jwt.user_id == '#uuid' + And match jwt.azp == '#string' + * def testUserId = jwt.user_id + + # Verify cross-module access resolution through sidecars: + # users-keycloak returns user data and effective permissions resolved from roles-keycloak. + * configure headers = { 'Content-Type': 'application/json', 'x-okapi-token': '#(loginToken)', 'Accept': '*/*', 'x-okapi-tenant': '#(testTenant)' } + Given path 'users-keycloak', '_self' + When method get + Then status 200 + And match response.user.id == testUserId + And match response.user.username == testUser.name + And match response.permissions.permissions contains upgradeRolePermission + + # Verify the pre-upgrade user can still be read by id. + Given path 'users-keycloak', 'users', testUserId + When method get + Then status 200 + And match response.id == testUserId + And match response.username == testUser.name + + # Verify the pre-upgrade role fixture still exists. + Given path 'roles' + And param query = 'name=="' + upgradeRoleName + '"' + When method get + Then status 200 + And match response.roles == '#[1]' + * def upgradeRoleId = response.roles[0].id + + # Verify the role still has the capability assigned before the upgrade. + Given path 'roles', upgradeRoleId, 'capabilities' + When method get + Then status 200 + And match response.capabilities[*].permission contains upgradeRolePermission + + # Verify effective permissions still include access contributed by the pre-upgrade role. + Given path 'permissions', 'users', testUserId + When method get + Then status 200 + And match response.permissions contains upgradeRolePermission + + # Verify refresh-token rotation still works after the upgrade. + * configure headers = null + * configure cookies = null + Given url baseUrl + And path 'authn', 'refresh' + And header x-okapi-tenant = testTenant + And cookie folioRefreshToken = refreshToken + When method post + Then status 201 + And match responseCookies.folioAccessToken == '#present' + And match responseCookies.folioRefreshToken == '#present' + * def refreshedToken = responseCookies.folioAccessToken.value + * def refreshedJwt = decodeJwtPayload(refreshedToken) + And match refreshedJwt.sub == testUser.name + And match refreshedJwt.user_id == testUserId + + # Verify the refreshed access token can be used against FOLIO APIs. + * configure headers = { 'Content-Type': 'application/json', 'x-okapi-token': '#(refreshedToken)', 'Accept': '*/*', 'x-okapi-tenant': '#(testTenant)' } + Given path 'users-keycloak', 'users', testUserId + When method get + Then status 200 + And match response.username == testUser.name + + # Verify tenant metadata can still be updated through the FOLIO tenant manager API. + * configure headers = null + * configure cookies = null + # Use the manager token only to authorize the FOLIO tenant manager request. + * def managerTokenResponse = call read('classpath:common/eureka/keycloak.feature@getKeycloakMasterToken') + * def managerToken = managerTokenResponse.response.access_token + + # Read the existing tenant before updating it. + Given path 'tenants', testTenantId + And header Authorization = 'Bearer ' + managerToken + When method get + Then status 200 + And match response.id == testTenantId + And match response.name == testTenant + * def tenant = response + * def updatedDescription = 'Updated by Keycloak upgrade IT ' + uuid() + * set tenant.description = updatedDescription + + # Update only the tenant description and verify the update response. + Given path 'tenants', testTenantId + And header Authorization = 'Bearer ' + managerToken + And header Content-Type = 'application/json' + And request tenant + When method put + Then status 200 + And match response.description == updatedDescription + + # Read the tenant again to confirm the description was persisted. + Given path 'tenants', testTenantId + And header Authorization = 'Bearer ' + managerToken + When method get + Then status 200 + And match response.description == updatedDescription diff --git a/keycloak-upgrade/src/main/resources/karate-config.js b/keycloak-upgrade/src/main/resources/karate-config.js new file mode 100644 index 0000000000..6eb146a0d3 --- /dev/null +++ b/keycloak-upgrade/src/main/resources/karate-config.js @@ -0,0 +1,103 @@ +function fn() { + + karate.configure('logPrettyRequest', true); + karate.configure('logPrettyResponse', true); + karate.configure('retry', { count: 20, interval: 15000 }); + + var env = karate.env; + var testTenant = karate.properties['testTenant']; + var testTenantId = karate.properties['testTenantId']; + var baseUrlOverride = karate.properties['baseUrl']; + var baseKeycloakUrlOverride = karate.properties['baseKeycloakUrl']; + var kcClientIdOverride = karate.properties['kcClientId']; + var kcClientSecretOverride = karate.properties['clientSecret']; + var m2mClientIdOverride = karate.properties['m2mClientId']; + var adminTenantOverride = karate.properties['admin.tenant']; + var adminNameOverride = karate.properties['admin.name']; + var adminPasswordOverride = karate.properties['admin.password']; + var effectiveTestTenant = testTenant ? testTenant : 'testtenant'; + var effectiveTestTenantId = testTenantId ? testTenantId : (function() { return java.util.UUID.randomUUID() + '' })(); + + var config = { + baseUrl: baseUrlOverride || 'http://localhost:8000', + baseKeycloakUrl: baseKeycloakUrlOverride || 'http://localhost:8080', + admin: { + tenant: adminTenantOverride || 'diku', + name: adminNameOverride || 'diku_admin', + password: adminPasswordOverride || 'admin' + }, + prototypeTenant: 'diku', + kcClientId: kcClientIdOverride || 'folio-backend-admin-client', + kcClientSecret: kcClientSecretOverride || 'SecretPassword', + m2mClientId: m2mClientIdOverride || 'sidecar-module-access-client', + tenantParams: { loadReferenceData: true }, + testTenant: effectiveTestTenant, + testTenantId: effectiveTestTenantId, + testUser: { tenant: effectiveTestTenant, name: 'kc-upgrade-user', password: 'test' }, + upgradeRoleName: 'kc-upgrade-role-' + effectiveTestTenant, + upgradeRoleDescription: 'Role created before a Keycloak upgrade and verified after restart', + upgradeRolePermission: 'roles.collection.get', + uuid: function () { + return java.util.UUID.randomUUID() + ''; + }, + decodeJwtPayload: function(token) { + var Base64 = Java.type('java.util.Base64'); + var String = Java.type('java.lang.String'); + var payload = token.split('.')[1]; + return karate.fromString(new String(Base64.getUrlDecoder().decode(payload))); + } + }; + + if (env == 'snapshot') { + config.baseUrl = 'https://folio-etesting-snapshot-kong.ci.folio.org'; + config.baseKeycloakUrl = 'https://folio-etesting-snapshot-keycloak.ci.folio.org'; + } else if (env == 'snapshot-2') { + config.baseUrl = 'https://folio-etesting-snapshot2-kong.ci.folio.org'; + config.baseKeycloakUrl = 'https://folio-etesting-snapshot2-keycloak.ci.folio.org'; + } else if (env == 'folio-testing-karate') { + config.baseUrl = '${baseUrl}'; + config.admin = { + tenant: '${admin.tenant}', + name: '${admin.name}', + password: '${admin.password}' + }; + config.kcClientId = '${clientId}'; + config.kcClientSecret = '${clientSecret}'; + config.prototypeTenant = '${prototypeTenant}'; + karate.configure('ssl', true); + config.baseKeycloakUrl = '${baseKeycloakUrl}'; + } else if (env == 'eureka1') { + config.baseUrl = 'https://folio-edev-eureka-kong.ci.folio.org'; + config.baseKeycloakUrl = 'https://folio-edev-eureka-keycloak.ci.folio.org'; + } else if (env == 'eureka2') { + config.baseUrl = 'https://folio-edev-eureka-2nd-kong.ci.folio.org'; + config.baseKeycloakUrl = 'https://folio-edev-eureka-2nd-keycloak.ci.folio.org'; + } else if (env == 'local') { + config.baseUrl = 'http://localhost:8000'; + config.baseKeycloakUrl = 'http://localhost:8080'; + config.kcClientSecret = 'folio-backend-admin-client-secret'; + config.m2mClientId = m2mClientIdOverride || 'm2m-client'; + } else if (env == 'dev') { + config.baseKeycloakUrl = 'http://keycloak.eureka:8080'; + config.kcClientId = 'supersecret'; + config.kcClientSecret = karate.properties['clientSecret'] || 'supersecret'; + } + + if (baseUrlOverride) { + config.baseUrl = baseUrlOverride; + } + if (baseKeycloakUrlOverride) { + config.baseKeycloakUrl = baseKeycloakUrlOverride; + } + if (kcClientIdOverride) { + config.kcClientId = kcClientIdOverride; + } + if (kcClientSecretOverride) { + config.kcClientSecret = kcClientSecretOverride; + } + if (m2mClientIdOverride) { + config.m2mClientId = m2mClientIdOverride; + } + + return config; +} diff --git a/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeSeedTests.java b/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeSeedTests.java new file mode 100644 index 0000000000..3c4ff721f4 --- /dev/null +++ b/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeSeedTests.java @@ -0,0 +1,28 @@ +package org.folio; + +import org.folio.test.TestBaseEureka; +import org.folio.test.annotation.FolioTest; +import org.folio.test.config.TestModuleConfiguration; +import org.folio.test.services.TestIntegrationService; +import org.junit.jupiter.api.Test; + +@FolioTest(team = "Eureka", module = "keycloak-upgrade") +class KeycloakUpgradeSeedTests extends TestBaseEureka { + + private static final String TEST_BASE_PATH = "classpath:eureka/keycloak-upgrade/features/"; + + KeycloakUpgradeSeedTests() { + super(new TestIntegrationService(new TestModuleConfiguration(TEST_BASE_PATH))); + } + + @Override + public void runHook() { + KeycloakUpgradeTenantState.prepareSeedTenant(); + super.runHook(); + } + + @Test + void seedBeforeUpgrade() { + runFeatureTest("seed-before-upgrade"); + } +} diff --git a/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeTenantState.java b/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeTenantState.java new file mode 100644 index 0000000000..5d5ccebf73 --- /dev/null +++ b/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeTenantState.java @@ -0,0 +1,130 @@ +package org.folio; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Properties; +import java.util.UUID; + +final class KeycloakUpgradeTenantState { + + private static final String TEST_TENANT = "testTenant"; + private static final String TEST_TENANT_ID = "testTenantId"; + private static final String STATE_FILE = "keycloak.upgrade.stateFile"; + private static final Path DEFAULT_STATE_FILE = Path.of("target", "keycloak-upgrade-tenant.properties"); + private static final String[] REQUIRED_STATE_KEYS = { + TEST_TENANT, TEST_TENANT_ID + }; + + private KeycloakUpgradeTenantState() { + } + + static void prepareSeedTenant() { + var testTenant = System.getProperty(TEST_TENANT); + var testTenantId = System.getProperty(TEST_TENANT_ID); + Properties state; + + if (isBlank(testTenant) && isBlank(testTenantId)) { + state = Files.exists(stateFile()) ? load() : new Properties(); + testTenant = state.getProperty(TEST_TENANT, randomTenantName("kcupgrade")); + testTenantId = state.getProperty(TEST_TENANT_ID, UUID.randomUUID().toString()); + } else if (isBlank(testTenant) || isBlank(testTenantId)) { + throw new IllegalStateException("Provide both -DtestTenant and -DtestTenantId, or omit both to auto-generate them"); + } else { + state = Files.exists(stateFile()) ? load() : new Properties(); + } + + state.setProperty(TEST_TENANT, testTenant); + state.setProperty(TEST_TENANT_ID, testTenantId); + setTenantProperties(state); + save(state); + } + + static void prepareVerifyTenant() { + var state = load(); + var testTenant = System.getProperty(TEST_TENANT); + var testTenantId = System.getProperty(TEST_TENANT_ID); + + if (isBlank(testTenant) != isBlank(testTenantId)) { + throw new IllegalStateException("Provide both -DtestTenant and -DtestTenantId, or omit both to load them from " + + stateFile().toAbsolutePath()); + } + + if (!isBlank(testTenant) && !testTenant.equals(state.getProperty(TEST_TENANT))) { + throw new IllegalStateException("Provided testTenant does not match seed state file: " + + stateFile().toAbsolutePath()); + } + if (!isBlank(testTenantId) && !testTenantId.equals(state.getProperty(TEST_TENANT_ID))) { + throw new IllegalStateException("Provided testTenantId does not match seed state file: " + + stateFile().toAbsolutePath()); + } + + validateState(state); + setTenantProperties(state); + } + + private static void save(Properties properties) { + var stateFile = stateFile(); + try { + var parent = stateFile.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + + properties.setProperty("createdAt", Instant.now().toString()); + + try (OutputStream outputStream = Files.newOutputStream(stateFile)) { + properties.store(outputStream, "Keycloak upgrade integration test tenant state"); + } + } catch (IOException exception) { + throw new IllegalStateException("Failed to write Keycloak upgrade tenant state to " + + stateFile.toAbsolutePath(), exception); + } + } + + private static Properties load() { + var stateFile = stateFile(); + if (!Files.exists(stateFile)) { + throw new IllegalStateException("Keycloak upgrade tenant state file does not exist: " + + stateFile.toAbsolutePath() + ". Run KeycloakUpgradeSeedTests first."); + } + + try (InputStream inputStream = Files.newInputStream(stateFile)) { + var properties = new Properties(); + properties.load(inputStream); + return properties; + } catch (IOException exception) { + throw new IllegalStateException("Failed to read Keycloak upgrade tenant state from " + + stateFile.toAbsolutePath(), exception); + } + } + + private static Path stateFile() { + var override = System.getProperty(STATE_FILE); + return isBlank(override) ? DEFAULT_STATE_FILE : Path.of(override); + } + + private static void validateState(Properties properties) { + for (String key : REQUIRED_STATE_KEYS) { + if (isBlank(properties.getProperty(key))) { + throw new IllegalStateException("Keycloak upgrade tenant state is missing " + key + " in " + + stateFile().toAbsolutePath() + ". Delete the state file and rerun KeycloakUpgradeSeedTests."); + } + } + } + + private static void setTenantProperties(Properties properties) { + properties.stringPropertyNames().forEach(name -> System.setProperty(name, properties.getProperty(name))); + } + + private static String randomTenantName(String prefix) { + return prefix + UUID.randomUUID().toString().replace("-", "").substring(0, 12); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } +} diff --git a/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeVerifyTests.java b/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeVerifyTests.java new file mode 100644 index 0000000000..d1741ad6f9 --- /dev/null +++ b/keycloak-upgrade/src/test/java/org/folio/KeycloakUpgradeVerifyTests.java @@ -0,0 +1,38 @@ +package org.folio; + +import org.folio.test.TestBaseEureka; +import org.folio.test.annotation.FolioTest; +import org.folio.test.config.TestModuleConfiguration; +import org.folio.test.services.TestIntegrationService; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +@FolioTest(team = "Eureka", module = "keycloak-upgrade") +class KeycloakUpgradeVerifyTests extends TestBaseEureka { + + private static final String TEST_BASE_PATH = "classpath:eureka/keycloak-upgrade/features/"; + private boolean verified; + + KeycloakUpgradeVerifyTests() { + super(new TestIntegrationService(new TestModuleConfiguration(TEST_BASE_PATH))); + } + + @Override + public void runHook() { + KeycloakUpgradeTenantState.prepareVerifyTenant(); + super.runHook(); + } + + @Test + void verifyAfterUpgrade() { + runFeatureTest("verify-after-upgrade"); + verified = true; + } + + @AfterAll + void tearDown() { + if (verified) { + runFeature("classpath:eureka/keycloak-upgrade/features/destroy-upgrade-data.feature"); + } + } +} diff --git a/pom.xml b/pom.xml index 91086b0689..c4b0046bbe 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,7 @@ mod-users-keycloak mod-login-keycloak mod-roles-keycloak + keycloak-upgrade mod-scheduler