From 86127f9dff4140f7dcda69a0bb6aeb109e008070 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 9 Dec 2024 12:01:52 -0800 Subject: [PATCH 01/24] add docker compose that allows all services to run locally 1. mysql on port 3306 2. postgresql on port 5432 3. ldap on port 389 and 636 (ssl) --- scripts/docker-compose.yml | 38 +++++++++++++++++++++++++++++++++++ scripts/mysql/init-db.sh | 31 ++++++++++++++++++++++++++++ scripts/postgresql/init-db.sh | 32 +++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 scripts/docker-compose.yml create mode 100755 scripts/mysql/init-db.sh create mode 100755 scripts/postgresql/init-db.sh diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml new file mode 100644 index 00000000000..e74ff57f17d --- /dev/null +++ b/scripts/docker-compose.yml @@ -0,0 +1,38 @@ +name: uaa + +services: + postgres: + image: "postgres:17.2" + ports: + - 5432:5432 + volumes: + - ./postgresql:/docker-entrypoint-initdb.d/ + environment: + - POSTGRES_PASSWORD=changeme + mysql: + image: "mysql:8" + ports: + - 3306:3306 + volumes: + - ./mysql:/docker-entrypoint-initdb.d/ + environment: + - MYSQL_ROOT_PASSWORD=changeme + openldap: + image: docker.io/bitnami/openldap:2.6 + ports: + - '389:1389' + - '636:1636' + # docs of these env vars: https://github.com/bitnami/containers/tree/2724f9cd02b3b4e7986a1e2a0b0b30af3737bbd2/bitnami/openldap#configuration + environment: + - LDAP_ROOT=dc=test,dc=com + - LDAP_ADMIN_USERNAME=admin + - LDAP_ADMIN_PASSWORD=password + - LDAP_USERS=user01,user02 + - LDAP_PASSWORDS=password1,password2 + - LDAP_GROUP=some-ldap-group + volumes: + - 'openldap_data:/bitnami/openldap' + +volumes: + openldap_data: + driver: local \ No newline at end of file diff --git a/scripts/mysql/init-db.sh b/scripts/mysql/init-db.sh new file mode 100755 index 00000000000..a82e1ab7185 --- /dev/null +++ b/scripts/mysql/init-db.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -euo pipefail + +# Number of gradle workers times 4, which was somewhat arbitrary but is sufficient in practice. +# We make extra dbs because a gradle worker ID can exceed the max number of workers. +NUM_OF_DATABASES_TO_CREATE=24 + + +function initDB() { + mysql -uroot -pchangeme <<-EOSQL + SET GLOBAL max_connections = 250; + DROP DATABASE IF EXISTS uaa; + CREATE DATABASE uaa DEFAULT CHARACTER SET utf8mb4; +EOSQL +} + +function createDB() { + DATABASE_NAME="uaa_${1}" + echo "Creating MySQL database with name ${DATABASE_NAME}" + mysql -uroot -pchangeme <<-EOSQL + DROP DATABASE IF EXISTS $DATABASE_NAME; + CREATE DATABASE $DATABASE_NAME DEFAULT CHARACTER SET utf8mb4; +EOSQL +} + +initDB + +for db_id in `seq 1 $NUM_OF_DATABASES_TO_CREATE`; do + createDB $db_id +done diff --git a/scripts/postgresql/init-db.sh b/scripts/postgresql/init-db.sh new file mode 100755 index 00000000000..2f65fcb7beb --- /dev/null +++ b/scripts/postgresql/init-db.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Number of gradle workers times 4, which was somewhat arbitrary but is sufficient in practice. +# We make extra dbs because a gradle worker ID can exceed the max number of workers. +NUM_OF_DATABASES_TO_CREATE=24 + +function initDB() { + psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DROP DATABASE IF EXISTS uaa; + CREATE DATABASE uaa; + DROP USER IF EXISTS root; + CREATE USER root WITH SUPERUSER PASSWORD '$POSTGRES_PASSWORD'; + SHOW max_connections; +EOSQL +} + +function createDB() { + DATABASE_NAME="uaa_${1}" + echo "Creating PostgreSQL database with name ${DATABASE_NAME}" + psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DROP DATABASE IF EXISTS $DATABASE_NAME; + CREATE DATABASE $DATABASE_NAME; +EOSQL +} + +initDB + +for db_id in `seq 1 $NUM_OF_DATABASES_TO_CREATE`; do + createDB $db_id +done \ No newline at end of file From fd5adcc84b5336c11a48c6bf0069fa9142f94c05 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 9 Dec 2024 15:06:18 -0800 Subject: [PATCH 02/24] Fix flakiness in a test that does not yield time change on a fast system --- .../identity/uaa/mock/token/TokenMvcMockTests.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index 08572b92723..86f5f30c826 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -1040,6 +1040,7 @@ void testClientIdentityProviderWithoutAllowedProvidersForPasswordGrantWorksInOth @Test void getToken_withPasswordGrantType_resultsInUserLastLogonTimestampUpdate() throws Exception { + long delayTime = 5; String username = "testuser" + generator.generate(); String userScopes = "uaa.user"; ScimUser user = setUpUser(jdbcScimUserProvisioning, jdbcScimGroupMembershipManager, jdbcScimGroupProvisioning, username, userScopes, OriginKeys.UAA, IdentityZone.getUaaZoneId()); @@ -1048,7 +1049,9 @@ void getToken_withPasswordGrantType_resultsInUserLastLogonTimestampUpdate() thro String accessToken = getAccessTokenForPasswordGrant(username); Long firstTimestamp = getPreviousLogonTime(accessToken); - + //simulate two sequential tests + //on a fast processor, there isn't enough granularity in the time + Thread.sleep(delayTime); String accessToken2 = getAccessTokenForPasswordGrant(username); Long secondTimestamp = getPreviousLogonTime(accessToken2); From ee707780eacc0c6480c37a5bd6fb0ec6f0f76493 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 9 Dec 2024 17:17:46 -0800 Subject: [PATCH 03/24] tests: fix unit tests to support all three databases - search for `@Disabled` to find tests commented out that don't work for MySQL - This is an initial stab at the problem and will be improved subsequently --- .../uaa/annotations/WithDatabaseContext.java | 1 - .../uaa/db/TableAndColumnNormalizationTest.java | 5 +++-- .../uaa/resources/jdbc/JdbcPagingListTests.java | 12 +++++++----- .../jdbc/JdbcScimGroupMembershipManagerTests.java | 7 +++++-- .../scim/jdbc/JdbcScimGroupProvisioningTests.java | 11 +++++++---- .../uaa/scim/jdbc/JdbcScimUserProvisioningTests.java | 2 ++ .../identity/uaa/DefaultTestContext.java | 1 - 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java b/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java index 14e683f7f67..957cf475c84 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java @@ -19,7 +19,6 @@ @Retention(RetentionPolicy.RUNTIME) @ExtendWith(SpringExtension.class) @ExtendWith(PollutionPreventionExtension.class) -@ActiveProfiles("default") @WebAppConfiguration @ContextConfiguration(classes = { DatabaseOnlyConfiguration.class, diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java index b0bbf6aaf0f..7a28af23972 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java @@ -12,6 +12,8 @@ import org.springframework.context.annotation.ImportResource; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.DisabledIf; +import org.springframework.test.context.junit.jupiter.EnabledIf; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.context.WebApplicationContext; @@ -36,7 +38,6 @@ class TableAndColumnNormalizationTestConfiguration { @ExtendWith(SpringExtension.class) @ExtendWith(PollutionPreventionExtension.class) -@ActiveProfiles("default") @WebAppConfiguration @ContextConfiguration(classes = { TableAndColumnNormalizationTestConfiguration.class, @@ -93,7 +94,7 @@ void checkColumns() throws Exception { logger.info("Checking column [" + name + "." + col + "]"); if (name != null && DatabaseInformation1_5_3.tableNames.contains(name.toLowerCase())) { logger.info("Validating column [" + name + "." + col + "]"); - assertEquals("Column[%s.%s] is not lower case.".formatted(name, col), col.toLowerCase(), col); + assertEquals(col.toLowerCase(), col, "Column[%s.%s] is not lower case.".formatted(name, col)); } } assertTrue(hadSomeResults, "Getting columns from db metadata should have returned some results"); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java index 7281b563886..a55e3c000cd 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java @@ -3,9 +3,10 @@ import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.JdbcTemplate; @@ -109,15 +110,16 @@ void jumpOverPages() { @Test void selectColumnsFull() { - list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT Foo.id, FOO.NAME from foo", new ColumnMapRowMapper(), 3); + list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT foo.id, foo.name from foo", new ColumnMapRowMapper(), 3); Map map = list.get(3); assertNotNull(map.get("name")); assertEquals("zab", map.get("name")); } @Test + @Disabled("Does not yet work on MySQL") void selectMoreColumnsWithOrderBy() { - list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT Foo.id, FOO.NAME FrOm foo wHere foo.name = 'FoO' OR foo.name = 'foo' OrDeR By foo.name", new ColumnMapRowMapper(), 3); + list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT foo.id, foo.name FrOm foo wHere foo.name = 'FoO' OR foo.name = 'foo' OrDeR By foo.name", new ColumnMapRowMapper(), 3); Map map = list.get(0); assertNotNull(map.get("name")); assertEquals("FoO", map.get("name")); @@ -128,10 +130,10 @@ void selectMoreColumnsWithOrderBy() { @Test void testWrongStatement() { - assertThrows(BadSqlGrammarException.class, + assertThrows(DataAccessException.class, () -> new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "Insert ('6', 'sab') from foo", new ColumnMapRowMapper(), 3)); - assertThrows(BadSqlGrammarException.class, + assertThrows(DataAccessException.class, () -> new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT * ", new ColumnMapRowMapper(), 3)); } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManagerTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManagerTests.java index 965d9a6c864..cd4660a6282 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManagerTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManagerTests.java @@ -96,6 +96,8 @@ class JdbcScimGroupMembershipManagerTests { @Autowired private PasswordEncoder passwordEncoder; + private String groupName; + @BeforeEach void setUp() throws SQLException { generator = new RandomValueStringGenerator(); @@ -103,6 +105,7 @@ void setUp() throws SQLException { uaaIdentityZone = IdentityZone.getUaa(); dbUtils = new DbUtils(); + groupName = dbUtils.getQuotedIdentifier("groups", jdbcTemplate); JdbcPagingListFactory pagingListFactory = new JdbcPagingListFactory(namedJdbcTemplate, limitSqlAdapter); JdbcScimUserProvisioning jdbcScimUserProvisioning = new JdbcScimUserProvisioning(namedJdbcTemplate, pagingListFactory, passwordEncoder, new IdentityZoneManagerImpl(), new JdbcIdentityZoneProvisioning(jdbcTemplate), new SimpleSearchQueryConverter(), new SimpleSearchQueryConverter(), new TimeServiceImpl(), true); @@ -369,7 +372,7 @@ void providerDeleted() throws SQLException { jdbcScimGroupProvisioning.onApplicationEvent(new EntityDeletedEvent<>(loginServer, null, anyZoneId)); - assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where group_id in (select id from groups where identity_zone_id=?) and origin=?", new Object[]{otherIdentityZone.getId(), LOGIN_SERVER}, Integer.class), is(0)); + assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where group_id in (select id from "+groupName+" where identity_zone_id=?) and origin=?", new Object[]{otherIdentityZone.getId(), LOGIN_SERVER}, Integer.class), is(0)); assertThat(jdbcTemplate.queryForObject("select count(*) from " + groups + " where identity_zone_id=?", new Object[]{otherIdentityZone.getId()}, Integer.class), is(3)); assertThat(jdbcTemplate.queryForObject("select count(*) from external_group_mapping where origin = ? and identity_zone_id=?", new Object[]{LOGIN_SERVER, otherIdentityZone.getId()}, Integer.class), is(0)); } @@ -379,7 +382,7 @@ void cannotDeleteUaaZone() throws SQLException { String groups = dbUtils.getQuotedIdentifier("groups", jdbcTemplate); addMembers(jdbcTemplate, uaaIdentityZone.getId()); - assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where group_id in (select id from groups where identity_zone_id=?)", new Object[]{uaaIdentityZone.getId()}, Integer.class), is(4)); + assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where group_id in (select id from "+groupName+" where identity_zone_id=?)", new Object[]{uaaIdentityZone.getId()}, Integer.class), is(4)); assertThat(jdbcTemplate.queryForObject("select count(*) from " + groups + " where identity_zone_id=?", new Object[]{uaaIdentityZone.getId()}, Integer.class), is(4)); jdbcScimGroupProvisioning.onApplicationEvent(new EntityDeletedEvent<>(IdentityZone.getUaa(), null, anyZoneId)); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java index 36d25ea0cef..03508f7900c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java @@ -73,6 +73,8 @@ class JdbcScimGroupProvisioningTests { @Autowired private LimitSqlAdapter limitSqlAdapter; + private String groupName; + private JdbcScimGroupProvisioning dao; private JdbcScimGroupMembershipManager memberships; private ScimUserProvisioning users; @@ -91,6 +93,9 @@ class JdbcScimGroupProvisioningTests { @BeforeEach void initJdbcScimGroupProvisioningTests() throws SQLException { + DbUtils dbUtils = new DbUtils(); + groupName = dbUtils.getQuotedIdentifier("groups", jdbcTemplate); + generator = new RandomValueStringGenerator(); SecureRandom random = new SecureRandom(); random.setSeed(System.nanoTime()); @@ -106,7 +111,6 @@ void initJdbcScimGroupProvisioningTests() throws SQLException { validateGroupCountInZone(0, zoneId); - DbUtils dbUtils = new DbUtils(); dao = spy(new JdbcScimGroupProvisioning(namedJdbcTemplate, new JdbcPagingListFactory(namedJdbcTemplate, limitSqlAdapter), dbUtils)); @@ -492,7 +496,7 @@ void sqlInjectionAttack3Fails() { void sqlInjectionAttack4Fails() { assertThrows( IllegalArgumentException.class, - () -> dao.query("displayName eq \"something\"; select id from groups where id='''; select " + SQL_INJECTION_FIELDS + () -> dao.query("displayName eq \"something\"; select id from "+groupName+" where id='''; select " + SQL_INJECTION_FIELDS + " from groups where displayName='something'", zoneId) ); } @@ -537,8 +541,7 @@ void deleteGroupByOrigin() { } private void validateGroupCountInZone(int expected, String zoneId) { - int existingGroupCount = jdbcTemplate.queryForObject("select count(id) from groups where identity_zone_id='" + zoneId + "'", Integer.class); - assertEquals(expected, existingGroupCount); + int existingGroupCount = jdbcTemplate.queryForObject("select count(id) from "+groupName+" where identity_zone_id='" + zoneId + "'", Integer.class); } private void validateGroup(ScimGroup group, String name, String zoneId) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java index 6c99bfb4818..362cbe62e12 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java @@ -28,6 +28,7 @@ import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManagerImpl; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -415,6 +416,7 @@ void retrieveByScimFilterUsingLower() { } @Test + @Disabled("Does not yet work on MySQL") void retrieveByScimFilter_IncludeInactive() { final String originActive = randomString(); addIdentityProvider(jdbcTemplate, currentIdentityZoneId, originActive, true); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java index eb10cd6bf05..0a6ee6c9725 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java @@ -25,7 +25,6 @@ @Retention(RetentionPolicy.RUNTIME) @ExtendWith(SpringExtension.class) @ExtendWith(PollutionPreventionExtension.class) -@ActiveProfiles("default") @WebAppConfiguration @ContextConfiguration(classes = { SpringServletTestConfig.class, From 3a8d0c61ff16d096ee898cd610633f63791e8fde Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Wed, 4 Dec 2024 18:16:10 +0100 Subject: [PATCH 04/24] tests: use java initializer in TableAndColumnNormalizationTest --- .../db/TableAndColumnNormalizationTest.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java index 7a28af23972..cc4ab86effc 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java @@ -1,5 +1,6 @@ package org.cloudfoundry.identity.uaa.db; +import org.cloudfoundry.identity.uaa.db.mysql.V1_5_4__NormalizeTableAndColumnNames; import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; import org.cloudfoundry.identity.uaa.util.beans.PasswordEncoderConfig; import org.junit.Assume; @@ -9,11 +10,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.core.env.MapPropertySource; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.DisabledIf; -import org.springframework.test.context.junit.jupiter.EnabledIf; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.context.WebApplicationContext; @@ -23,26 +24,47 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.util.Arrays; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @ImportResource(locations = { "classpath:spring/env.xml", - "classpath:spring/use_uaa_db_in_mysql_url.xml", // adds this one "classpath:spring/jdbc-test-base-add-flyway.xml", "classpath:spring/data-source.xml", }) class TableAndColumnNormalizationTestConfiguration { } +/** + * For MySQL, the database name is hardcoded in the {@link V1_5_4__NormalizeTableAndColumnNames} migration as + * {@code uaa}. But the {@link UaaDatabaseName} class dynamically allocates a DB name based on the gradle worker id, + * like {@code uaa_1, uaa_2m ...}. + *

+ * When the profile is {@code mysql}, hardcode the DB url to have the database name equal to {@code uaa}. + */ +class MySQLInitializer implements ApplicationContextInitializer { + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + var profiles = Arrays.asList(applicationContext.getEnvironment().getActiveProfiles()); + if (profiles.contains("mysql")) { + Map dynamicProperties = Map.of("database.url", "jdbc:mysql://127.0.0.1:3306/uaa?useSSL=true&trustServerCertificate=true"); + MapPropertySource propertySource = new MapPropertySource("mysql-override", dynamicProperties); + applicationContext.getEnvironment().getPropertySources().addLast(propertySource); + } + } +} + @ExtendWith(SpringExtension.class) @ExtendWith(PollutionPreventionExtension.class) @WebAppConfiguration @ContextConfiguration(classes = { TableAndColumnNormalizationTestConfiguration.class, - PasswordEncoderConfig.class, -}) + PasswordEncoderConfig.class +}, + initializers = MySQLInitializer.class +) class TableAndColumnNormalizationTest { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -100,4 +122,5 @@ void checkColumns() throws Exception { assertTrue(hadSomeResults, "Getting columns from db metadata should have returned some results"); } } + } From b81e0cc294ef7c55d97891b5a63276abc7e86a6d Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Mon, 9 Dec 2024 15:08:28 +0100 Subject: [PATCH 05/24] tests: introduce @EnabledIfProfile / @DisabledIfProfile - Useful in database testing, allows enabling/disabling tests based on whether `postgresql` or `mysql` is present in the list of active profiles. --- .../db/DbMigrationIntegrationTestParent.java | 21 ++--- .../db/HsqlDbMigrationIntegrationTest.java | 11 +-- .../db/MySqlDbMigrationIntegrationTest.java | 12 +-- .../PostgresDbMigrationIntegrationTest.java | 12 +-- .../db/TableAndColumnNormalizationTest.java | 15 +--- .../profiles/DisabledIfProfile.java | 49 +++++++++++ .../extensions/profiles/EnabledIfProfile.java | 50 +++++++++++ .../profiles/ProfileSelectionExtension.java | 86 +++++++++++++++++++ 8 files changed, 206 insertions(+), 50 deletions(-) create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/DisabledIfProfile.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/EnabledIfProfile.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/ProfileSelectionExtension.java diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java index 608d3b70f16..54eed0a3b06 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java @@ -2,20 +2,18 @@ import org.cloudfoundry.identity.uaa.test.TestUtils; import org.flywaydb.core.Flyway; -import org.junit.After; -import org.junit.Before; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import java.sql.SQLException; -import static java.lang.System.getProperties; -import static org.junit.Assume.assumeTrue; -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(locations = { "classpath:spring/env.xml", "classpath:spring/jdbc-test-base-add-flyway.xml", @@ -31,19 +29,14 @@ public abstract class DbMigrationIntegrationTestParent { MigrationTestRunner migrationTestRunner; private boolean dbNeedsResetting; - protected abstract String onlyRunTestsForActiveSpringProfileName(); - - @Before + @BeforeEach public void setup() { - String active = getProperties().getProperty("spring.profiles.active"); - assumeTrue("Expected db profile to be enabled", active != null && active.contains(onlyRunTestsForActiveSpringProfileName())); - dbNeedsResetting = true; flyway.clean(); migrationTestRunner = new MigrationTestRunner(flyway); } - @After + @AfterEach public void cleanup() throws SQLException { if (dbNeedsResetting) { // cleanup() is always called, even when setup()'s assumeTrue() fails // Avoid test pollution by putting the db back into a default state that other tests assume diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java index eefcc2e8e8b..c225830e6c3 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java @@ -1,27 +1,24 @@ package org.cloudfoundry.identity.uaa.db; -import org.junit.Test; +import org.cloudfoundry.identity.uaa.extensions.profiles.DisabledIfProfile; +import org.junit.jupiter.api.Test; import java.util.List; -import static junit.framework.TestCase.fail; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.fail; +@DisabledIfProfile({"mysql", "postgresql"}) public class HsqlDbMigrationIntegrationTest extends DbMigrationIntegrationTestParent { private final String checkPrimaryKeyExists = "SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = ? AND TABLE_NAME = UPPER(?) AND CONSTRAINT_NAME LIKE 'SYS_PK_%'"; private final String getAllTableNames = "SELECT distinct TABLE_NAME from information_schema.KEY_COLUMN_USAGE where TABLE_SCHEMA = ? and TABLE_NAME != 'schema_version'"; private final String insertNewOauthCodeRecord = "insert into oauth_code(code) values('code');"; - @Override - protected String onlyRunTestsForActiveSpringProfileName() { - return "hsqldb"; - } - @Test public void insertMissingPrimaryKeys_onMigrationOnNewDatabase() { MigrationTest migrationTest = new MigrationTest() { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java index fb6cb269cb1..31ccfd584f2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java @@ -1,29 +1,25 @@ package org.cloudfoundry.identity.uaa.db; -import org.junit.Test; +import org.cloudfoundry.identity.uaa.extensions.profiles.EnabledIfProfile; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; -import static junit.framework.TestCase.fail; -import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.fail; +@EnabledIfProfile("mysql") public class MySqlDbMigrationIntegrationTest extends DbMigrationIntegrationTestParent { private final String checkPrimaryKeyExists = "SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'"; private final String getAllTableNames = "SELECT distinct TABLE_NAME from information_schema.KEY_COLUMN_USAGE where TABLE_SCHEMA = ?"; private final String insertNewOauthCodeRecord = "insert into oauth_code(code) values('code');"; - @Override - protected String onlyRunTestsForActiveSpringProfileName() { - return "mysql"; - } - @Test public void insertMissingPrimaryKeys_onMigrationOnNewDatabase() { MigrationTest migrationTest = new MigrationTest() { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java index 30b059261cd..d84f1583a88 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java @@ -1,27 +1,23 @@ package org.cloudfoundry.identity.uaa.db; -import org.junit.Test; +import org.cloudfoundry.identity.uaa.extensions.profiles.EnabledIfProfile; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.fail; +@EnabledIfProfile("postgresql") public class PostgresDbMigrationIntegrationTest extends DbMigrationIntegrationTestParent { private final String checkPrimaryKeyExists = "SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_CATALOG = ? AND TABLE_NAME = LOWER(?) AND CONSTRAINT_NAME LIKE LOWER(?)"; private final String getAllTableNames = "SELECT distinct TABLE_NAME from information_schema.KEY_COLUMN_USAGE where TABLE_CATALOG = ? and TABLE_NAME != 'schema_version' AND TABLE_SCHEMA != 'pg_catalog'"; private final String insertNewOauthCodeRecord = "insert into oauth_code(code) values('code');"; - @Override - protected String onlyRunTestsForActiveSpringProfileName() { - return "postgresql"; - } - @Test public void everyTableShouldHaveAPrimaryKeyColumn() throws Exception { flyway.migrate(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java index cc4ab86effc..f8c3a55d9e1 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/TableAndColumnNormalizationTest.java @@ -2,9 +2,8 @@ import org.cloudfoundry.identity.uaa.db.mysql.V1_5_4__NormalizeTableAndColumnNames; import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; +import org.cloudfoundry.identity.uaa.extensions.profiles.EnabledIfProfile; import org.cloudfoundry.identity.uaa.util.beans.PasswordEncoderConfig; -import org.junit.Assume; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; @@ -17,7 +16,6 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.web.context.WebApplicationContext; import javax.sql.DataSource; import java.sql.Connection; @@ -65,6 +63,7 @@ public void initialize(ConfigurableApplicationContext applicationContext) { }, initializers = MySQLInitializer.class ) +@EnabledIfProfile({"postgresql", "mysql"}) class TableAndColumnNormalizationTest { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -72,16 +71,6 @@ class TableAndColumnNormalizationTest { @Autowired private DataSource dataSource; - @BeforeEach - void checkMysqlOrPostgresqlProfile( - @Autowired WebApplicationContext webApplicationContext - ) { - Assume.assumeTrue( - Arrays.asList(webApplicationContext.getEnvironment().getActiveProfiles()).contains("mysql") || - Arrays.asList(webApplicationContext.getEnvironment().getActiveProfiles()).contains("postgresql") - ); - } - @Test void checkTables() throws Exception { try (Connection connection = dataSource.getConnection()) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/DisabledIfProfile.java b/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/DisabledIfProfile.java new file mode 100644 index 00000000000..8a29a8e5d85 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/DisabledIfProfile.java @@ -0,0 +1,49 @@ +package org.cloudfoundry.identity.uaa.extensions.profiles; + + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Disable the test if any of the given profiles are present in the TestContext. + *

+ * Annotate test methods or test classes. In case there are nested annotations, e.g. + * one on the method and one on the class, the closest annotation is taken into account. + *

+ * When the {@link EnabledIfProfile} element is present, this annotation takes precedence. + *

+ * Usage: + * + *

+ * @ExtendWith(SpringExtension.class)
+ * @ContextConfiguration(...)
+ * // ...
+ * class MyAppTests {
+ *      @Test
+ *      void runsEveryTime() {
+ *          // ...
+ *      }
+ *
+ *      @Test
+ *      @DisabledIfProfile({"postgresql", "mysql"})
+ *      void doNotRunForRealDb() {
+ *          // Does not run when the active profiles contain "postgresql" or "mysql"
+ *      }
+ * }
+ * 
+ * + * @see ProfileSelectionExtension + * @see EnabledIfProfile + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(ProfileSelectionExtension.class) +public @interface DisabledIfProfile { + + String[] value() default {}; + +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/EnabledIfProfile.java b/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/EnabledIfProfile.java new file mode 100644 index 00000000000..3e64fecc90e --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/EnabledIfProfile.java @@ -0,0 +1,50 @@ +package org.cloudfoundry.identity.uaa.extensions.profiles; + + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Enable the test only when any of the given profiles are present in the TestContext. + *

+ * Annotate test methods or test classes. In case there are nested annotations, e.g. + * one on the method and one on the class, the closest annotation is taken into account. + *

+ * When the {@link DisabledIfProfile} annotation is also present, {@link DisabledIfProfile} + * takes precedence. + *

+ * Usage: + * + *

+ * @ExtendWith(SpringExtension.class)
+ * @ContextConfiguration(...)
+ * // ...
+ * class MyAppTests {
+ *      @Test
+ *      void runsEveryTime() {
+ *          // ...
+ *      }
+ *
+ *      @Test
+ *      @EnabledIfProfile({"postgresql", "mysql"})
+ *      void onlyRunForRealDb() {
+ *          // Only runs when the active profiles contain "postgresql" or "mysql"
+ *      }
+ * }
+ * 
+ * + * @see ProfileSelectionExtension + * @see DisabledIfProfile + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(ProfileSelectionExtension.class) +public @interface EnabledIfProfile { + + String[] value() default {}; + +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/ProfileSelectionExtension.java b/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/ProfileSelectionExtension.java new file mode 100644 index 00000000000..0d7775014ff --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/extensions/profiles/ProfileSelectionExtension.java @@ -0,0 +1,86 @@ +package org.cloudfoundry.identity.uaa.extensions.profiles; + + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Arrays; +import java.util.List; + +/** + * Extension to disable tests based on whether a specific profile is active or not. + * Disabling profiles takes precedence over enabling profiles. + * Annotations on methods take precedence over annotations on classes. + * + * @see DisabledIfProfile + * @see EnabledIfProfile + */ +public class ProfileSelectionExtension implements BeforeTestExecutionCallback { + + @Override + public void beforeTestExecution(ExtensionContext context) { + var activeProfiles = getActiveProfilesOrNull(context); + if (activeProfiles == null) { + return; + } + + // Method, disabled + var disabledIfProfile = context.getTestMethod() + .flatMap(method -> AnnotationUtils.findAnnotation(method, DisabledIfProfile.class)); + if (disabledIfProfile.isPresent()) { + skipIfExcludedProfilePresent(disabledIfProfile.get(), activeProfiles); + return; + } + + // Method, enabled + var enabledIfProfile = context.getTestMethod() + .flatMap(method -> AnnotationUtils.findAnnotation(method, EnabledIfProfile.class)); + if (enabledIfProfile.isPresent()) { + skipIfProfileMissing(enabledIfProfile.get(), activeProfiles); + return; + } + + // Class, disabled + var disabledIfProfileClass = context.getTestClass() + .flatMap(c -> AnnotationUtils.findAnnotation(c, DisabledIfProfile.class, true)); + if (disabledIfProfileClass.isPresent()) { + skipIfExcludedProfilePresent(disabledIfProfileClass.get(), activeProfiles); + return; + } + + // Class, enabled + var enabledIfProfileClass = context.getTestClass() + .flatMap(c -> AnnotationUtils.findAnnotation(c, EnabledIfProfile.class, true)); + if (enabledIfProfileClass.isPresent()) { + skipIfProfileMissing(enabledIfProfileClass.get(), activeProfiles); + return; + } + } + + private List getActiveProfilesOrNull(ExtensionContext context) { + try { + var applicationContext = SpringExtension.getApplicationContext(context); + return Arrays.asList(applicationContext.getEnvironment().getActiveProfiles()); + } catch (IllegalStateException ignore) { + return null; + } + } + + private static void skipIfExcludedProfilePresent(DisabledIfProfile annotation, List activeProfiles) { + var excludedProfiles = Arrays.asList(annotation.value()); + var hasExcludedProfile = excludedProfiles.stream().anyMatch(activeProfiles::contains); + var message = "Must NOT have one of the following profiles: %s. Active profiles: %s.".formatted(excludedProfiles, activeProfiles); + Assumptions.assumeTrue(!hasExcludedProfile, message); + } + + private static void skipIfProfileMissing(EnabledIfProfile annotation, List activeProfiles) { + var requiredProfile = Arrays.asList(annotation.value()); + var hasRequiredProfile = requiredProfile.stream().anyMatch(activeProfiles::contains); + var message = "Must have one of the following profiles: %s. Active profiles: %s.".formatted(requiredProfile, activeProfiles); + Assumptions.assumeTrue(hasRequiredProfile, message); + } + +} From 80de45e405b887195e49b142be18d0f6ab0bf51c Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Mon, 9 Dec 2024 15:09:06 +0100 Subject: [PATCH 06/24] tests: fix hsql-specific JdbcPagingListTests --- .../uaa/annotations/WithDatabaseContext.java | 1 - .../resources/jdbc/JdbcPagingListTests.java | 44 ++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java b/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java index 957cf475c84..7d485e548ec 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/annotations/WithDatabaseContext.java @@ -5,7 +5,6 @@ import org.cloudfoundry.identity.uaa.util.beans.PasswordEncoderConfig; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java index a55e3c000cd..f89ccf2ca38 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/resources/jdbc/JdbcPagingListTests.java @@ -1,12 +1,14 @@ package org.cloudfoundry.identity.uaa.resources.jdbc; import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext; +import org.cloudfoundry.identity.uaa.extensions.profiles.DisabledIfProfile; +import org.cloudfoundry.identity.uaa.extensions.profiles.EnabledIfProfile; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.JdbcTemplate; @@ -17,6 +19,7 @@ import java.util.Map; import java.util.Set; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -33,8 +36,8 @@ class JdbcPagingListTests { @BeforeEach public void initJdbcPagingListTests(@Autowired DataSource dataSource) { - jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("drop table if exists foo"); jdbcTemplate.execute("create table foo (id integer primary key, name varchar(10) not null)"); jdbcTemplate.execute("insert into foo (id, name) values (0, 'foo')"); jdbcTemplate.execute("insert into foo (id, name) values (1, 'bar')"); @@ -116,10 +119,13 @@ void selectColumnsFull() { assertEquals("zab", map.get("name")); } + /** + * HSQL-db has a different ordering from postgres and mysql + */ @Test - @Disabled("Does not yet work on MySQL") - void selectMoreColumnsWithOrderBy() { - list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT foo.id, foo.name FrOm foo wHere foo.name = 'FoO' OR foo.name = 'foo' OrDeR By foo.name", new ColumnMapRowMapper(), 3); + @DisabledIfProfile({"postgresql", "mysql"}) + void selectMoreColumnsWithOrderBy_Hsql() { + list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT foo.id, foo.NAME FrOm foo wHere foo.name = 'FoO' OR foo.name = 'foo' OrDeR By foo.name", new ColumnMapRowMapper(), 3); Map map = list.get(0); assertNotNull(map.get("name")); assertEquals("FoO", map.get("name")); @@ -129,12 +135,28 @@ void selectMoreColumnsWithOrderBy() { } @Test - void testWrongStatement() { - assertThrows(DataAccessException.class, - () -> new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "Insert ('6', 'sab') from foo", new ColumnMapRowMapper(), 3)); + @EnabledIfProfile({"postgresql", "mysql"}) + void selectMoreColumnsWithOrderBy_PostgresMysql() { + list = new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT foo.id, foo.NAME FrOm foo wHere foo.name = 'FoO' OR foo.name = 'foo' OrDeR By foo.name", new ColumnMapRowMapper(), 3); + Map map = list.get(0); + assertNotNull(map.get("name")); + assertEquals("foo", map.get("name")); + map = list.get(1); + assertNotNull(map.get("name")); + assertEquals("FoO", map.get("name")); + } - assertThrows(DataAccessException.class, - () -> new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT * ", new ColumnMapRowMapper(), 3)); + @Test + void testWrongStatement() { + assertThatThrownBy + (() -> new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "Insert ('6', 'sab') from foo", new ColumnMapRowMapper(), 3)) + .isInstanceOf(BadSqlGrammarException.class); + + assertThatThrownBy(() -> new JdbcPagingList<>(jdbcTemplate, limitSqlAdapter, "SELECT * ", new ColumnMapRowMapper(), 3)) + .isInstanceOfAny( + BadSqlGrammarException.class, // hsqldb, postgres + TransientDataAccessResourceException.class // mysql + ); } @Test From 0596320451b7fb91a5cd833138c8741e2aac92f4 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 10 Dec 2024 12:33:55 +0100 Subject: [PATCH 07/24] tests: fix JdbcScimUserProvisioningTests.retrieveByScimFilter_IncludeInactive for mysql --- .../scim/jdbc/JdbcScimUserProvisioningTests.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java index 362cbe62e12..864f1af2cf0 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java @@ -28,13 +28,13 @@ import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManagerImpl; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; @@ -47,6 +47,7 @@ import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -107,6 +108,10 @@ class JdbcScimUserProvisioningTests { @Autowired NamedParameterJdbcTemplate namedJdbcTemplate; + + @Autowired + private Environment enviroment; + private String joeEmail; private static final String JOE_NAME = "joe"; @@ -416,7 +421,6 @@ void retrieveByScimFilterUsingLower() { } @Test - @Disabled("Does not yet work on MySQL") void retrieveByScimFilter_IncludeInactive() { final String originActive = randomString(); addIdentityProvider(jdbcTemplate, currentIdentityZoneId, originActive, true); @@ -443,7 +447,11 @@ void retrieveByScimFilter_IncludeInactive() { ); Assertions.assertThat(result).isNotNull(); final List usernames = result.stream().map(ScimUser::getUserName).toList(); - Assertions.assertThat(usernames).isSorted(); + if (Arrays.stream(enviroment.getActiveProfiles()).noneMatch("mysql"::equalsIgnoreCase)) { + // MySQL has a different ordering from HSQL and Postgres. In MySQL the two values + // we inserted are sorted in reverse order, so we skip the assert entirely + Assertions.assertThat(usernames).isSorted(); + } return usernames; }; From 2ca7f14552fcccae57b1e53c12d973d490089aca Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 10 Dec 2024 16:03:07 +0100 Subject: [PATCH 08/24] tests: fix JdbcClientMetadataProvisioningTest.createdByPadsTo36Chars with mysql --- scripts/docker-compose.yml | 2 ++ .../uaa/client/JdbcClientMetadataProvisioningTest.java | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index e74ff57f17d..e1fa599154f 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -17,6 +17,8 @@ services: - ./mysql:/docker-entrypoint-initdb.d/ environment: - MYSQL_ROOT_PASSWORD=changeme + command: + - --sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,PAD_CHAR_TO_FULL_LENGTH openldap: image: docker.io/bitnami/openldap:2.6 ports: diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index ec79b276163..220123d809a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -71,6 +71,13 @@ void constraintViolation_WhenNoMatchingClientFound() throws Exception { () -> jdbcClientMetadataProvisioning.update(clientMetadata, identityZoneId)); } + /** + * In MySQL, characters are stored with a padding, but when they are retrieved, the padding is trimmed. + * To disable this behavior, you must add the {@code sql_mode} to include {@code PAD_CHAR_TO_FULL_LENGTH}. + * + * @see CHAR type docs + * @see PAD_CHAR_TO_FULL_LENGTH + */ @Test void createdByPadsTo36Chars() { jdbcTemplate.execute(insertIntoOauthClientDetails(clientId, identityZoneId, "abcdef")); From 2ae24396a3db812762a0957406b8aba3767d56d8 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 10 Dec 2024 13:59:35 -0800 Subject: [PATCH 09/24] Remove hard coded database values from the MockMvc integration tests --- .../identity/uaa/DefaultTestContext.java | 5 +---- .../test/resources/integration_test_properties.yml | 14 -------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java index 0a6ee6c9725..14d3ae5ff84 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java @@ -36,10 +36,7 @@ @ImportResource(locations = {"file:./src/main/webapp/WEB-INF/spring-servlet.xml"}) @PropertySource(value = "classpath:integration_test_properties.yml", factory = NestedMapPropertySourceFactory.class) class SpringServletTestConfig { - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } + } class TestClientAndMockMvcTestConfig { diff --git a/uaa/src/test/resources/integration_test_properties.yml b/uaa/src/test/resources/integration_test_properties.yml index 72cf004ccae..dff69d04db5 100644 --- a/uaa/src/test/resources/integration_test_properties.yml +++ b/uaa/src/test/resources/integration_test_properties.yml @@ -126,20 +126,6 @@ login: authorize: url: http://localhost:8080/uaa/oauth/authorize -database: - abandonedtimeout: 45 - caseinsensitive: true - evictionintervalms: 30000 - logabandoned: false - maxactive: 50 - maxidle: 5 - minidle: 3 - removeabandoned: true - driverClassName: org.hsqldb.jdbcDriver - url: jdbc:hsqldb:mem:uaadb - username: sa - password: '' - ldap: profile: file: ldap/ldap-search-and-bind.xml From c0162f548d248a19abf205d5c0e4d9e605cb1016 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 10 Dec 2024 14:02:03 -0800 Subject: [PATCH 10/24] Downgrade MySQL to avoid padding issues Test that fail: JdbcApprovalStoreTests.deleteProviderDeletesApprovals Why: - Table users has column id char(30) - Table authz_approvals has column user_id varchar(30) These two don't match when joining in a query Suspect: https://dev.mysql.com/worklog/task/?id=12129 --- scripts/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index e1fa599154f..187f297fbc9 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -10,7 +10,7 @@ services: environment: - POSTGRES_PASSWORD=changeme mysql: - image: "mysql:8" + image: "mysql:5.7.44-oraclelinux7" ports: - 3306:3306 volumes: From 97f475abb627b31c6e01990ce5e3435843a7a5e3 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 11 Dec 2024 10:21:27 -0800 Subject: [PATCH 11/24] Remove database connections leaks (1 prod, many in tests) --- .../mysql/V4_9_2__AddPrimaryKeysIfMissing.java | 14 +++++++++++++- .../uaa/db/DbMigrationIntegrationTestParent.java | 4 ++++ .../uaa/db/HsqlDbMigrationIntegrationTest.java | 12 ++++++------ .../identity/uaa/db/MigrationTest.java | 14 ++++++++++++++ .../uaa/db/MySqlDbMigrationIntegrationTest.java | 16 ++++++++-------- .../db/PostgresDbMigrationIntegrationTest.java | 4 ++-- 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V4_9_2__AddPrimaryKeysIfMissing.java b/server/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V4_9_2__AddPrimaryKeysIfMissing.java index 92e6222b6e6..e9235855d34 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V4_9_2__AddPrimaryKeysIfMissing.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V4_9_2__AddPrimaryKeysIfMissing.java @@ -4,6 +4,10 @@ import org.flywaydb.core.api.migration.Context; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.SingleConnectionDataSource; +import org.springframework.jdbc.datasource.lookup.DataSourceLookupFailureException; + +import java.sql.Connection; +import java.sql.SQLException; public class V4_9_2__AddPrimaryKeysIfMissing extends BaseJavaMigration { @@ -16,11 +20,19 @@ public void migrate(Context context) throws Exception { JdbcTemplate jdbcTemplate = new JdbcTemplate(new SingleConnectionDataSource( context.getConnection(), true)); for (String table : tables) { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), table); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(jdbcTemplate), table); if (count == 0) { String sql = "ALTER TABLE " + table + " ADD COLUMN `id` int(11) unsigned PRIMARY KEY AUTO_INCREMENT"; jdbcTemplate.execute(sql); } } } + + private String getDatabaseCatalog(JdbcTemplate jdbcTemplate) { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + return connection.getCatalog(); + } catch (SQLException e) { + throw new DataSourceLookupFailureException("Unable to look up database schema.", e); + } + } } \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java index 54eed0a3b06..4f0cdf7d639 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/DbMigrationIntegrationTestParent.java @@ -45,4 +45,8 @@ public void cleanup() throws SQLException { TestUtils.cleanAndSeedDb(jdbcTemplate); } } + + protected String getDatabaseCatalog() { + return MigrationTest.getDatabaseCatalog(jdbcTemplate); + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java index c225830e6c3..4c4ea40cc2a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/HsqlDbMigrationIntegrationTest.java @@ -29,16 +29,16 @@ public String getTargetMigration() { @Override public void runAssertions() throws Exception { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "OAUTH_CODE"); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "OAUTH_CODE"); assertThat("OAUTH_CODE is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "GROUP_MEMBERSHIP"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "GROUP_MEMBERSHIP"); assertThat("GROUP_MEMBERSHIP is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "SEC_AUDIT"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "SEC_AUDIT"); assertThat("SEC_AUDIT is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "EXTERNAL_GROUP_MAPPING"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "EXTERNAL_GROUP_MAPPING"); assertThat("EXTERNAL_GROUP_MAPPING is missing primary key", count, is(1)); try { @@ -56,10 +56,10 @@ public void runAssertions() throws Exception { public void everyTableShouldHaveAPrimaryKeyColumn() throws Exception { flyway.migrate(); - List tableNames = jdbcTemplate.queryForList(getAllTableNames, String.class, jdbcTemplate.getDataSource().getConnection().getCatalog()); + List tableNames = jdbcTemplate.queryForList(getAllTableNames, String.class, getDatabaseCatalog()); assertThat(tableNames, hasSize(greaterThan(0))); for (String tableName : tableNames) { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), tableName); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), tableName); assertThat("%s is missing primary key".formatted(tableName), count, greaterThanOrEqualTo(1)); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/MigrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/MigrationTest.java index 80575df6c9a..b99b0ec5cbf 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/MigrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/MigrationTest.java @@ -1,7 +1,21 @@ package org.cloudfoundry.identity.uaa.db; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.lookup.DataSourceLookupFailureException; + +import java.sql.Connection; +import java.sql.SQLException; + public interface MigrationTest { String getTargetMigration(); void runAssertions() throws Exception; + + static String getDatabaseCatalog(JdbcTemplate jdbcTemplate) { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + return connection.getCatalog(); + } catch (SQLException e) { + throw new DataSourceLookupFailureException("Unable to look up database schema.", e); + } + } } \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java index 31ccfd584f2..4942d800886 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/MySqlDbMigrationIntegrationTest.java @@ -30,16 +30,16 @@ public String getTargetMigration() { @Override public void runAssertions() throws Exception { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "oauth_code"); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "oauth_code"); assertThat("oauth_code is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "group_membership"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "group_membership"); assertThat("group_membership is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "sec_audit"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "sec_audit"); assertThat("sec_audit is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "external_group_mapping"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "external_group_mapping"); assertThat("external_group_membership is missing primary key", count, is(1)); try { @@ -79,10 +79,10 @@ public String getTargetMigration() { @Override public void runAssertions() throws Exception { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "group_membership"); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "group_membership"); assertThat("group_membership is missing primary key", count, is(1)); - count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), "external_group_mapping"); + count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), "external_group_mapping"); assertThat("external_group_mapping is missing primary key", count, is(1)); } }); @@ -94,10 +94,10 @@ public void runAssertions() throws Exception { public void everyTableShouldHaveAPrimaryKeyColumn() throws Exception { flyway.migrate(); - List tableNames = jdbcTemplate.queryForList(getAllTableNames, String.class, jdbcTemplate.getDataSource().getConnection().getCatalog()); + List tableNames = jdbcTemplate.queryForList(getAllTableNames, String.class, getDatabaseCatalog()); assertThat(tableNames, hasSize(greaterThan(0))); for (String tableName : tableNames) { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), tableName); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), tableName); assertThat("%s is missing primary key".formatted(tableName), count, greaterThanOrEqualTo(1)); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java index d84f1583a88..610b60a258c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/PostgresDbMigrationIntegrationTest.java @@ -22,10 +22,10 @@ public class PostgresDbMigrationIntegrationTest extends DbMigrationIntegrationTe public void everyTableShouldHaveAPrimaryKeyColumn() throws Exception { flyway.migrate(); - List tableNames = jdbcTemplate.queryForList(getAllTableNames, String.class, jdbcTemplate.getDataSource().getConnection().getCatalog()); + List tableNames = jdbcTemplate.queryForList(getAllTableNames, String.class, getDatabaseCatalog()); assertThat(tableNames, hasSize(greaterThan(0))); for (String tableName : tableNames) { - int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, jdbcTemplate.getDataSource().getConnection().getCatalog(), tableName, "%" + tableName + "_pk%"); + int count = jdbcTemplate.queryForObject(checkPrimaryKeyExists, Integer.class, getDatabaseCatalog(), tableName, "%" + tableName + "_pk%"); assertThat("%s is missing primary key".formatted(tableName), count, greaterThanOrEqualTo(1)); } From 5d33664c738f647b102a78735dcfa0b9b15f3b94 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 11 Dec 2024 13:52:59 -0800 Subject: [PATCH 12/24] Change table columns `users.id` to varchar from char - Some tests fail on mysql. For example, authz_approvals.user_id is varchar fails when joining against a padded char column. - The problem does not happen in prod because user.id is generated in java and is always a UUIDv4, which is 36 characters. - The tests only fail with MySQL because of the lack of padding. We only migrate MySQL because we would like to avoid migrations altogether. --- .../identity/uaa/db/hsqldb/V4_112__Users_id_to_Varchar_MySQL.sql | 1 + .../identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql | 1 + .../uaa/db/postgresql/V4_112__Users_id_to_Varchar_MySQL.sql | 1 + 3 files changed, 3 insertions(+) create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_112__Users_id_to_Varchar_MySQL.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_112__Users_id_to_Varchar_MySQL.sql diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_112__Users_id_to_Varchar_MySQL.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_112__Users_id_to_Varchar_MySQL.sql new file mode 100644 index 00000000000..888918292a5 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_112__Users_id_to_Varchar_MySQL.sql @@ -0,0 +1 @@ +-- NOOP \ No newline at end of file diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql new file mode 100644 index 00000000000..c55880c9cc7 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql @@ -0,0 +1 @@ +ALTER TABLE users MODIFY id VARCHAR(36) NOT NULL; \ No newline at end of file diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_112__Users_id_to_Varchar_MySQL.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_112__Users_id_to_Varchar_MySQL.sql new file mode 100644 index 00000000000..888918292a5 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_112__Users_id_to_Varchar_MySQL.sql @@ -0,0 +1 @@ +-- NOOP \ No newline at end of file From 0eb383aa8566b665f4a25aefbc63b0dc95b0e7d5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 11 Dec 2024 13:55:12 -0800 Subject: [PATCH 13/24] Ensure that the table `groups` is properly escaped since groups is a keyword in SQL Now we can use MySQL 8 Downgrade to PostgreSQL 15 (need to find out exactly what the test matrix should be) --- scripts/docker-compose.yml | 4 ++-- .../V2_7_3__StoreSubDomainAsLowerCase_Tests.java | 16 +++++++++++++--- .../identity/uaa/scim/test/TestUtils.java | 11 ++++++++++- .../uaa/db/TestZonifyGroupSchema_V2_4_1.java | 9 ++++++--- .../zones/IdentityZoneEndpointsMockMvcTests.java | 7 ++++++- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index 187f297fbc9..df3828e9677 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -2,7 +2,7 @@ name: uaa services: postgres: - image: "postgres:17.2" + image: "postgres:15" ports: - 5432:5432 volumes: @@ -10,7 +10,7 @@ services: environment: - POSTGRES_PASSWORD=changeme mysql: - image: "mysql:5.7.44-oraclelinux7" + image: "mysql:8" ports: - 3306:3306 volumes: diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/db/V2_7_3__StoreSubDomainAsLowerCase_Tests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/db/V2_7_3__StoreSubDomainAsLowerCase_Tests.java index 2677019b529..b65bf39609a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/db/V2_7_3__StoreSubDomainAsLowerCase_Tests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/db/V2_7_3__StoreSubDomainAsLowerCase_Tests.java @@ -6,6 +6,7 @@ import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.flywaydb.core.api.migration.Context; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -13,6 +14,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.cloudfoundry.identity.uaa.oauth.common.util.RandomValueStringGenerator; +import java.sql.Connection; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Arrays; @@ -36,15 +38,23 @@ class V2_7_3__StoreSubDomainAsLowerCase_Tests { @Autowired private JdbcTemplate jdbcTemplate; + private Connection connection; + + @AfterEach + void closeConnection() { + try { + connection.close(); + } catch (Exception ignore) { + } + } @BeforeEach void setUpDuplicateZones() throws SQLException { provisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate); migration = new V2_7_3__StoreSubDomainAsLowerCase(); generator = new RandomValueStringGenerator(6); - + connection = jdbcTemplate.getDataSource().getConnection(); context = mock(Context.class); - when(context.getConnection()).thenReturn( - jdbcTemplate.getDataSource().getConnection()); + when(context.getConnection()).thenReturn(connection); } @Test diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/test/TestUtils.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/test/TestUtils.java index 2b21acb82ba..6dc56e9a57f 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/test/TestUtils.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/test/TestUtils.java @@ -1,8 +1,10 @@ package org.cloudfoundry.identity.uaa.scim.test; import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.util.beans.DbUtils; import org.springframework.jdbc.core.JdbcTemplate; +import java.sql.SQLException; import java.util.Collections; import java.util.stream.Stream; @@ -14,8 +16,15 @@ public class TestUtils { public static void deleteFrom( final JdbcTemplate jdbcTemplate, final String... tables) { + DbUtils dbUtils = new DbUtils(); Stream.of(tables) - .map(table -> "delete from " + table) + .map(table -> { + try { + return "delete from " + dbUtils.getQuotedIdentifier(table, jdbcTemplate); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }) .forEach(jdbcTemplate::update); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/db/TestZonifyGroupSchema_V2_4_1.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/db/TestZonifyGroupSchema_V2_4_1.java index cc45d0e5a63..ac0d0951ade 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/db/TestZonifyGroupSchema_V2_4_1.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/db/TestZonifyGroupSchema_V2_4_1.java @@ -6,6 +6,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.endpoints.ScimGroupEndpoints; import org.cloudfoundry.identity.uaa.scim.endpoints.ScimUserEndpoints; +import org.cloudfoundry.identity.uaa.util.beans.DbUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneEndpoints; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -91,9 +92,11 @@ protected Object getActualFieldValue(String field) { } @Test - void test_Ensure_That_New_Fields_NotNull() { - assertThat(webApplicationContext.getBean(JdbcTemplate.class).queryForObject("SELECT count(*) FROM external_group_mapping WHERE origin IS NULL", Integer.class), is(0)); - assertThat(webApplicationContext.getBean(JdbcTemplate.class).queryForObject("SELECT count(*) FROM groups WHERE identity_zone_id IS NULL", Integer.class), is(0)); + void test_Ensure_That_New_Fields_NotNull() throws Exception { + JdbcTemplate jdbcTemplate = webApplicationContext.getBean(JdbcTemplate.class); + DbUtils dbUtils = webApplicationContext.getBean(DbUtils.class); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM external_group_mapping WHERE origin IS NULL", Integer.class), is(0)); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM "+ dbUtils.getQuotedIdentifier("groups", jdbcTemplate) +" WHERE identity_zone_id IS NULL", Integer.class), is(0)); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java index ed6b7087243..21f4e2cc2b0 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java @@ -38,6 +38,7 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.KeyWithCertTest; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; +import org.cloudfoundry.identity.uaa.util.beans.DbUtils; import org.cloudfoundry.identity.uaa.zone.BrandingInformation; import org.cloudfoundry.identity.uaa.zone.BrandingInformation.Banner; import org.cloudfoundry.identity.uaa.zone.Consent; @@ -172,6 +173,8 @@ class IdentityZoneEndpointsMockMvcTests { private MockMvc mockMvc; private TestClient testClient; + private DbUtils dbUtils; + @BeforeEach void setUp( @Autowired WebApplicationContext webApplicationContext, @@ -186,6 +189,8 @@ void setUp( this.mockMvc = mockMvc; this.testClient = testClient; + dbUtils = webApplicationContext.getBean(DbUtils.class); + UaaClientDetails uaaAdminClient = new UaaClientDetails("uaa-admin-" + generator.generate().toLowerCase(), null, "uaa.admin", @@ -1737,7 +1742,7 @@ void test_delete_zone_cleans_db() throws Exception { assertThat(template.queryForObject("select count(*) from identity_zone where id=?", new Object[]{zone.getId()}, Integer.class)).isZero(); assertThat(template.queryForObject("select count(*) from oauth_client_details where identity_zone_id=?", new Object[]{zone.getId()}, Integer.class)).isZero(); - assertThat(template.queryForObject("select count(*) from groups where identity_zone_id=?", new Object[]{zone.getId()}, Integer.class)).isZero(); + assertThat(template.queryForObject("select count(*) from "+dbUtils.getQuotedIdentifier("groups", template)+" where identity_zone_id=?", new Object[]{zone.getId()}, Integer.class)).isZero(); assertThat(template.queryForObject("select count(*) from sec_audit where identity_zone_id=?", new Object[]{zone.getId()}, Integer.class)).isZero(); assertThat(template.queryForObject("select count(*) from users where identity_zone_id=?", new Object[]{zone.getId()}, Integer.class)).isZero(); assertThat(template.queryForObject("select count(*) from external_group_mapping where origin=?", new Object[]{LOGIN_SERVER}, Integer.class)).isZero(); From 49f7a30ee0fa21658c161b180ecc18347f6e6b0c Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 11 Dec 2024 14:50:52 -0800 Subject: [PATCH 14/24] add TODO in ScimUserEndpointsAliasMockMvcTests.AliasFeatureEnabled --- .../uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java index 39e79136218..d9585f5b2c4 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java @@ -242,6 +242,7 @@ private void shouldAccept_ShouldCreateAliasUser( final ScimUser createdScimUser = createScimUser(zone1, scimUser); // find alias user + //TODO this is flaky final List usersZone2 = readRecentlyCreatedUsersInZone(zone2); final Optional aliasUserOpt = usersZone2.stream() .filter(user -> user.getId().equals(createdScimUser.getAliasId())) From 4b02a1b6d06534796aac8dad31637a2071ca16d5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 11 Dec 2024 15:57:47 -0800 Subject: [PATCH 15/24] No more CHAR datatypes, only varchar --- .../identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql | 3 ++- .../uaa/client/JdbcClientMetadataProvisioningTest.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql index c55880c9cc7..a9d79af25f4 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql @@ -1 +1,2 @@ -ALTER TABLE users MODIFY id VARCHAR(36) NOT NULL; \ No newline at end of file +ALTER TABLE users MODIFY id VARCHAR(36) NOT NULL; +ALTER TABLE oauth_client_details MODIFY created_by VARCHAR(36) NOT NULL; \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index 220123d809a..9101d9d63a7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -4,6 +4,7 @@ import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; @@ -79,6 +80,7 @@ void constraintViolation_WhenNoMatchingClientFound() throws Exception { * @see PAD_CHAR_TO_FULL_LENGTH */ @Test + @Disabled("SQL Migration script 4.112 changes this column to varchar because MySQL deprecated PAD_CHAR_TO_FULL_LENGTH") void createdByPadsTo36Chars() { jdbcTemplate.execute(insertIntoOauthClientDetails(clientId, identityZoneId, "abcdef")); From eece47633d3d2e78ad1ab4dce71d877367746b3b Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 11 Dec 2024 16:07:11 -0800 Subject: [PATCH 16/24] Fix mistake on column null handling --- .../identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql index a9d79af25f4..e7d1da410d5 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_112__Users_id_to_Varchar.sql @@ -1,2 +1,2 @@ ALTER TABLE users MODIFY id VARCHAR(36) NOT NULL; -ALTER TABLE oauth_client_details MODIFY created_by VARCHAR(36) NOT NULL; \ No newline at end of file +ALTER TABLE oauth_client_details MODIFY created_by VARCHAR(36) NULL; \ No newline at end of file From fea1a59515199deaee5a31270e904d9d3e907706 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Fri, 13 Dec 2024 08:30:56 -0800 Subject: [PATCH 17/24] tests: disable BootstrapTests in mysql and postgresql profiles - BootstrapTests rely on HSQL as they only use the default profile, therefore we do not need to run them in postgres or mysql mode. They would run against HSQL even in those cases. --- .../org/cloudfoundry/identity/uaa/login/BootstrapTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index 198d6fda70a..be02d27d5a3 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -5,6 +5,7 @@ import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; import org.cloudfoundry.identity.uaa.extensions.SpringProfileCleanupExtension; import org.cloudfoundry.identity.uaa.extensions.SystemPropertiesCleanupExtension; +import org.cloudfoundry.identity.uaa.extensions.profiles.DisabledIfProfile; import org.cloudfoundry.identity.uaa.impl.config.IdentityZoneConfigurationBootstrap; import org.cloudfoundry.identity.uaa.impl.config.YamlServletProfileInitializer; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; @@ -57,6 +58,7 @@ @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(SpringProfileCleanupExtension.class) +@DisabledIfProfile({"mysql", "postgresql"}) class BootstrapTests { private static final String LOGIN_IDP_METADATA = "login.idpMetadata"; private static final String LOGIN_IDP_ENTITY_ALIAS = "login.idpEntityAlias"; From 03b544e330c1525cebda1fc7ae435ccb2cc1717e Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Mon, 16 Dec 2024 16:13:28 +0100 Subject: [PATCH 18/24] tests: remove duplicate test - Duplicate of `Branding#testExternalizedBranding` --- .../identity/uaa/login/LoginMockMvcTests.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index d81532fa95b..8c7c25ffb37 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -627,17 +627,6 @@ void testLogin_When_DisableInternalUserManagement_Is_True() throws Exception { MockMvcUtils.setDisableInternalUserManagement(webApplicationContext, false); } - @Nested - @DefaultTestContext - @TestPropertySource(properties = "assetBaseUrl=//cdn.example.com/resources") - class DefaultLogo { - @Test - void testDefaultLogo(@Autowired MockMvc mockMvc) throws Exception { - mockMvc.perform(get("/login")) - .andExpect(content().string(containsString("url(//cdn.example.com/resources/images/product-logo.png)"))); - } - } - @Test void testCustomLogo() throws Exception { setZoneFavIconAndProductLogo(webApplicationContext, identityZoneConfiguration, null, "/bASe/64+"); From 34656be3699ca17497fd5b72f44928a68afb7a8c Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Mon, 16 Dec 2024 17:16:19 +0100 Subject: [PATCH 19/24] tests: ensure mysql is on the same timezone as the host machine when TZ is set --- scripts/docker-compose.yml | 2 ++ scripts/mysql/init-db.sh | 25 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index df3828e9677..945709ac7e1 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -15,8 +15,10 @@ services: - 3306:3306 volumes: - ./mysql:/docker-entrypoint-initdb.d/ + - /etc/localtime:/localtime-from-host environment: - MYSQL_ROOT_PASSWORD=changeme + - TZ=${TZ} command: - --sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,PAD_CHAR_TO_FULL_LENGTH openldap: diff --git a/scripts/mysql/init-db.sh b/scripts/mysql/init-db.sh index a82e1ab7185..274a419e64a 100755 --- a/scripts/mysql/init-db.sh +++ b/scripts/mysql/init-db.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail @@ -6,7 +6,6 @@ set -euo pipefail # We make extra dbs because a gradle worker ID can exceed the max number of workers. NUM_OF_DATABASES_TO_CREATE=24 - function initDB() { mysql -uroot -pchangeme <<-EOSQL SET GLOBAL max_connections = 250; @@ -24,8 +23,28 @@ function createDB() { EOSQL } +function setTimezone() { + # If the "TZ" env var is set in the container definition, then set the + # DB at the given timezone. When "TZ" is unset, use the DB default (UTC). + # + # This is important because the database should run in the same timezone as the UAA, + # and, in the case of tests, the same timezone as the JVM running the tests. + # + # We achieve consistency by changing the timezone inside the DB; because setting + # it in the container is complicated. The container is missing the `timedatectl` + # binary ; and the script runs as the mysql user which does not have sudo privileges. + if [[ -n "$TZ" ]]; then + echo "Setting DB timezone to: $TZ" + mysql -uroot -pchangeme <<-EOSQL + SET GLOBAL time_zone = "$TZ"; +EOSQL + fi +} + + + initDB for db_id in `seq 1 $NUM_OF_DATABASES_TO_CREATE`; do createDB $db_id -done +done \ No newline at end of file From 9436ed40de02787ce2a1655917f4e05e6c8b481d Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 16 Dec 2024 15:07:09 -0800 Subject: [PATCH 20/24] Fix connection leak from Flyway --- .../identity/uaa/db/beans/FlywayConfiguration.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/db/beans/FlywayConfiguration.java b/server/src/main/java/org/cloudfoundry/identity/uaa/db/beans/FlywayConfiguration.java index 610461d7e04..04091c7e6d5 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/db/beans/FlywayConfiguration.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/db/beans/FlywayConfiguration.java @@ -63,6 +63,9 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) public Flyway flyway(Flyway baseFlyway) { baseFlyway.repair(); baseFlyway.migrate(); + org.apache.tomcat.jdbc.pool.DataSource ds = + (org.apache.tomcat.jdbc.pool.DataSource)baseFlyway.getConfiguration().getDataSource(); + ds.purge(); return baseFlyway; } } From 428b1892ea7899bb65905596188541bc87e19d50 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 17 Dec 2024 11:20:12 +0100 Subject: [PATCH 21/24] tests: database data volumes in docker-compose.yml uses tmpfs - Ensure the data is only stored in memory and not persisted across container restarts. - It means we do not need to delete the volumes between runs. - It requires 1GB memory per database. --- scripts/docker-compose.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index 945709ac7e1..067f338be39 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -7,18 +7,26 @@ services: - 5432:5432 volumes: - ./postgresql:/docker-entrypoint-initdb.d/ + - type: tmpfs + target: /var/lib/postgresql/data + tmpfs: + size: 1073741824 # 1024 * 2^20 bytes = 1024Mb environment: - POSTGRES_PASSWORD=changeme mysql: image: "mysql:8" ports: - - 3306:3306 + - 3306:3306 volumes: - - ./mysql:/docker-entrypoint-initdb.d/ - - /etc/localtime:/localtime-from-host + - ./mysql:/docker-entrypoint-initdb.d/ + - /etc/localtime:/localtime-from-host + - type: tmpfs + target: /var/lib/mysql + tmpfs: + size: 1073741824 # 1024 * 2^20 bytes = 1024Mb environment: - - MYSQL_ROOT_PASSWORD=changeme - - TZ=${TZ} + - MYSQL_ROOT_PASSWORD=changeme + - TZ=${TZ} command: - --sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,PAD_CHAR_TO_FULL_LENGTH openldap: From de56eabb9a3ec1e340756ef2120c8568921d9911 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 17 Dec 2024 15:05:55 +0100 Subject: [PATCH 22/24] tests: re-enable createdByPadsTo36Chars excpet for MySQL --- .../uaa/client/JdbcClientMetadataProvisioningTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index 9101d9d63a7..53eadbf5b0e 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -1,10 +1,10 @@ package org.cloudfoundry.identity.uaa.client; import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext; +import org.cloudfoundry.identity.uaa.extensions.profiles.DisabledIfProfile; import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; @@ -80,7 +80,7 @@ void constraintViolation_WhenNoMatchingClientFound() throws Exception { * @see PAD_CHAR_TO_FULL_LENGTH */ @Test - @Disabled("SQL Migration script 4.112 changes this column to varchar because MySQL deprecated PAD_CHAR_TO_FULL_LENGTH") + @DisabledIfProfile("mysql") void createdByPadsTo36Chars() { jdbcTemplate.execute(insertIntoOauthClientDetails(clientId, identityZoneId, "abcdef")); From 8738e292258728c608b122567672c663be5bc178 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 17 Dec 2024 16:15:39 +0100 Subject: [PATCH 23/24] docs: add docs/testing.md --- README.md | 21 +++++++- docs/testing.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index d2f78d70b28..96c151c4981 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ You can run the integration tests with docker will create a docker container running uaa + ldap + database whereby integration tests are run against. -### Using Gradle to test with postgresql or mysql +### Using Docker to test with postgresql or mysql The default uaa unit tests (./gradlew test integrationTest) use hsqldb. @@ -217,6 +217,25 @@ To run the unit tests with docker: $ run-unit-tests.sh +### Using Gradle to test with Postgres or MySQL + +You need a locally running database. You can launch a Postgres 15 and MySQL 8 locally with docker compose: + + $ docker compose --file scripts/docker-compose.yaml up + +If you wish to launch only one of the DBs, select the appropriate service name: + + $ docker compose --file scripts/docker-compose.yaml up postgresql + +Then run the test with the appropriate profile: + + $ ./gradlew '-Dspring.profiles.active=postgresql,default' \ + --no-daemon \ + test + +There are special guarantees in place to avoid pollution between tests, so be sure to run the images +from the compose script, and run your test with `--no-daemon`. To learn more, read [docs/testing.md](docs/testing.md). + ### To run a single test The default uaa unit tests (`./gradlew test`) use hsqldb. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000000..f1445d89bf2 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,136 @@ +# Testing considerations + +This document contains information about how the tests are structured and why. + +## Types of test + +There are two types of tests: + +- Unit tests, which run tests on classes or subsets of the application in a single JVM. Those run with `./gradlew test`. +- Integration tests, which launch the UAA application and run web-based tests the running app. Those can be run with + `./gradlew integrationTest` + +## Helper scripts + +There are helper scripts, `run-unit-tests.sh` and `run-integration-tests.sh`, which run the tests inside a docker +container. The docker container they run in contains the database against which to run the tests, as well as an LDAP +server. It is self-contained but lacks flexibility. It relies on custom-baked image that may not support arm64, and +can't work with your IDE. + +However, since the scripts run a container with all dependencies, you do not need infrastructure to run against a +specified DB: + + $ run-unit-tests.sh + +## Test databases + +By default, the tests run against an in-memory DB, `hsqldb`. This DB is also present in the prod artifact, so that +UAA can also be ran standalone to test tweaks in a live instance. + +To run these databases locally, use the docker-compose script: + + $ docker compose --file scripts/docker-compose.yaml up + +If you wish to launch only one of the DBs, select the appropriate service name: + + $ docker compose --file scripts/docker-compose.yaml up postgresql + +To run tests against either Postgres or MySQL, use the `postgresql` or `mysql` profile, to select the DB. Be sure +to add the `default` profile which will trigger seeding the database with some admin users, clients, etc. For example: + + $ ./gradlew '-Dspring.profiles.active=mysql,default test + +To run tests from your IDE against a given database, you can (temporarily) annotate the test class: + +```java + +@ActiveProfiles({"mysql", "default"}) +class MyCustomTests { + @Test + void foo() { + // ... + } +} +``` + +## Database-specific tests + +Some tests only work on a single type of database, for example `MySqlDbMigrationIntegrationTest`; or do not work on a +given database, for example `JdbcClientMetadataProvisioningTest.createdByPadsTo36Chars`. You can turn tests on and off +based on the profile with custom annotations, `@DisabledIfProfile` and `@EnabledIfProfile`, for example: + +```java +// Only run on either mysql or postgresql +@EnabledIfProfile({"mysql", "postgresql"}) +class RealDbOnlyTests { + +} + +// or: + +class SomeTests { + + // Do not run when there is either mysql or postgresql + @DisabledIfProfile({"mysql", "postgres"}) + void notOnRealDb() { + + } + +} +``` + +## Test pollution + +There might be test pollution when tests are run in parallel, or even between projects. For example, when you run + + $ ./gradlew test + +It will run tests in both `cloudfoundry-identity-server` and `cloudfoundry-identity-uaa` projects. Both need a database, +and both do sometimes clean up the database. + +To avoid test pollution, 24 databases are created, and each Gradle "worker" thread gets its own database. A Gradle +worker has a numeric `id`, and each time a new task is spun up, the idea of the worker picking up the task increases. +So there are 24 DBs with names `uaa_1`, `uaa_2`, ... created, and usually the worker ID stays below 24 and there are +enough databases for each test. + +However, if the gradle daemon is kept running in the background and is re-used for subsequent tasks, e.g. by doing: + + $ ./gradlew test # first run + # do some code changes + $ ./gradlew test + +You will get new workers with IDs > 24. It is recommended you run your Gradle in no-daemon mode when running tests: + + $ ./gradlew test --no-daemon + +It will be slightly slower to start up (a few seconds), but the tests take multiple minutes and so the gain of using +a daemon is not worth the trouble. + +## Timezone issues + +The UAA and its DB server _MUST_ have the same timezone, because dates are not uniformly stored in UTC and timezones +do matter. Specifically for MySQL, there are issues when your local host is ahead of UTC, because: + +1. The default containers runs in UTC +2. When calling `current_timestamp` the value is in UTC +3. But when calling a prepared statement from JDBC with a Date/Timestamp/time-based the timezone is sent to the server + +So, when running e.g. in `Europe/Paris` (CET): + +```java +jdbcTemplate.queryForObject("SELECT CURRENT_TIMESTAMP",String .class); +// will return 15:00UTC +// if the TZ is dropped, it is recorded as 15:00 +jdbcTemplate.update("UPDATE foo SET updated=?",new Date(System.currentTimeMillis())); +// will insert 16:00CET +// if the TZ is dropped this is recorded as 16:00 +``` + +For this reason, we update the MySQL container in `docker-compose.yml` to have the same timezone as the host through +the `$TZ` env var. + +If you have timing-based issues in your test, ensure that you set `$TZ` before starting docker compose, e.g.: + + $ TZ="Europe/Paris" docker compose up + +It is not required, and MySQL will default to using UTC. \ No newline at end of file From 345c7c164118cac2fd62dd8e90ce3960255177c6 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 17 Dec 2024 22:20:03 +0100 Subject: [PATCH 24/24] tests: rework JdbcScimUserProvisioningTests.retrieveByScimFilter_IncludeInactive to compare by "created" - this better matches what is actually done in prod, and avoids sorting by username where mysql has a different opinion of alphabetical ordering of `jo@` and `jo2@`. --- .../jdbc/JdbcScimUserProvisioningTests.java | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java index 864f1af2cf0..b2ce3356cd1 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioningTests.java @@ -47,8 +47,9 @@ import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -438,42 +439,37 @@ void retrieveByScimFilter_IncludeInactive() { user2.setOrigin(originInactive); final ScimUser created2 = jdbcScimUserProvisioning.createUser(user2, "j7hyqpassX", currentIdentityZoneId); - final Function> retrieveByScimFilter = scimFilter -> { - final List result = jdbcScimUserProvisioning.query( - scimFilter, - "userName", - true, - currentIdentityZoneId - ); - Assertions.assertThat(result).isNotNull(); - final List usernames = result.stream().map(ScimUser::getUserName).toList(); - if (Arrays.stream(enviroment.getActiveProfiles()).noneMatch("mysql"::equalsIgnoreCase)) { - // MySQL has a different ordering from HSQL and Postgres. In MySQL the two values - // we inserted are sorted in reverse order, so we skip the assert entirely - Assertions.assertThat(usernames).isSorted(); - } - return usernames; - }; + final Function> retrieveByScimFilter = scimFilter -> jdbcScimUserProvisioning.query( + scimFilter, + "created", + true, + currentIdentityZoneId + ); // case 1: should return both String filter = "id eq '%s' or origin eq '%s'".formatted(created1.getId(), created2.getOrigin()); - List usernames = retrieveByScimFilter.apply(filter); - Assertions.assertThat(usernames) + List users = retrieveByScimFilter.apply(filter); + var oldestFirst = Comparator.comparing(x -> x.getMeta().getCreated()); + Assertions.assertThat(users) .hasSize(2) + .isSortedAccordingTo(oldestFirst) + .map(ScimUser::getUserName) .contains(created1.getUserName(), created2.getUserName()); // case 2: should return user 2 filter = "origin eq '%s'".formatted(created2.getOrigin()); - usernames = retrieveByScimFilter.apply(filter); - Assertions.assertThat(usernames) + users = retrieveByScimFilter.apply(filter); + Assertions.assertThat(users) .hasSize(1) + .map(ScimUser::getUserName) .contains(created2.getUserName()); // case 3: should return user 2 (filtered by origin and ID) filter = "origin eq '%s' and id eq '%s'".formatted(created2.getOrigin(), created2.getId()); - usernames = retrieveByScimFilter.apply(filter); - Assertions.assertThat(usernames) + users = retrieveByScimFilter.apply(filter); + Assertions.assertThat(users) .hasSize(1) + .map(ScimUser::getUserName) .contains(created2.getUserName()); }