Skip to content

Commit

Permalink
Introduce @DatabaseTestUTC for timezone-sensitive DB tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Kehrlann committed Dec 13, 2024
1 parent ce3c127 commit a5a0075
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package org.cloudfoundry.identity.uaa.audit;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext;
import org.cloudfoundry.identity.uaa.extensions.timezone.WithTimeZone;
import org.cloudfoundry.identity.uaa.extensions.database.DatabaseTestUTC;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -25,6 +26,9 @@ class JdbcAuditServiceTests {
@Autowired
private JdbcTemplate jdbcTemplate;

@Autowired
private DataSource dataSource;

@BeforeEach
void createService() {
auditService = new JdbcAuditService(jdbcTemplate);
Expand Down Expand Up @@ -56,7 +60,7 @@ void principalAuthenticationFailureAuditSucceeds() {
}

@Test
@WithTimeZone(WithTimeZone.UTC)
@DatabaseTestUTC
void findMethodOnlyReturnsEventsWithinRequestedPeriod() {
long now = System.currentTimeMillis();
auditService.log(getAuditEvent(PrincipalAuthenticationFailure, "clientA"), getAuditEvent(PrincipalAuthenticationFailure, "clientA").getIdentityZoneId());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.cloudfoundry.identity.uaa.audit;

import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext;
import org.cloudfoundry.identity.uaa.extensions.timezone.WithTimeZone;
import org.cloudfoundry.identity.uaa.extensions.database.DatabaseTestUTC;
import org.cloudfoundry.identity.uaa.util.TimeService;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -21,7 +21,6 @@
import static org.cloudfoundry.identity.uaa.audit.AuditEventType.UserAuthenticationFailure;
import static org.cloudfoundry.identity.uaa.audit.AuditEventType.UserAuthenticationSuccess;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -127,7 +126,7 @@ void userPasswordChangeSuccessResetsData() {
}

@Test
@WithTimeZone(WithTimeZone.UTC)
@DatabaseTestUTC
void findMethodOnlyReturnsEventsWithinRequestedPeriod() {
long now = System.currentTimeMillis();
when(mockTimeService.getCurrentTimeMillis()).thenReturn(now);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.cloudfoundry.identity.uaa.extensions.database;


import org.cloudfoundry.identity.uaa.extensions.timezone.WithTimeZone;
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;

/**
* Force the JVM timezone to be UTC for the current test, and reset all
* database connections. This is required because MySQL and Possgres have
* different behavior when it comes to timezone. When passing a time parameter
* value to a JDBC template behaves in the same way, it is converted to the
* JVM timezone. But calling {@code current_timestamp} has different behavior:
*
* <ul>
* <li>In MySQL, it returns UTC-based time.</li>
* <li>In Possgres, it returns timezone-sensitive timestamp. The selected timezone is the JVM timezone <b>whenever the connection was first opened</b>.</li>
* </ul>
* Timezone-sensitive tests are broken in MySQL when ran in a timezone ahead of UTC,
* unless the JVM timezone is forced to be UTC.
* <p>
* Timezone-sensitive tests are broken in Postgres when the timezone is forced to UTC
* right before a test and you are in a timezone behind UTC.
* <p>
* This annotation forces the timezone to be UTC, and then resets all connections, so that
* Possgres connections match the JVM timezone. After a test, timezone is restored and
* connections are reset once more.
* <p>
* It sets the correct order for {@link WithTimeZone} and {@link ResetDatabaseConnectionsExtension},
* so that the TimeZone is always correct before connections are reset.
*
* @see WithTimeZone
* @see ResetDatabaseConnectionsExtension
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@WithTimeZone(WithTimeZone.UTC)
@ExtendWith(ResetDatabaseConnectionsExtension.class)
public @interface DatabaseTestUTC {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.cloudfoundry.identity.uaa.extensions.database;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.lang.Nullable;
import org.springframework.test.context.junit.jupiter.SpringExtension;

/**
* Extension to reset database connections before and after a test.
* It closes all existing connections. This is particularly useful when the
* database connection sets session properties when they are opened.
*
* @see DatabaseTestUTC
*/
class ResetDatabaseConnectionsExtension implements BeforeEachCallback, AfterEachCallback {

@Override
public void beforeEach(ExtensionContext context) throws Exception {
var dataSource = getDataSourceOrNull(context);
if (dataSource == null) {
return;
}
dataSource.getPool().purge();
}

@Override
public void afterEach(ExtensionContext context) throws Exception {
var dataSource = getDataSourceOrNull(context);
if (dataSource == null) {
return;
}
dataSource.getPool().purge();
}


@Nullable
private DataSource getDataSourceOrNull(ExtensionContext context) {
try {
var applicationContext = SpringExtension.getApplicationContext(context);
return applicationContext.getBean(DataSource.class);
} catch (IllegalStateException ignore) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.cloudfoundry.identity.uaa.extensions.timezone;


import org.cloudfoundry.identity.uaa.extensions.profiles.EnabledIfProfile;
import org.junit.jupiter.api.extension.ExtendWith;

import java.lang.annotation.ElementType;
Expand Down Expand Up @@ -37,10 +36,9 @@
* }
* </pre>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TimeZoneExtension.class)
@EnabledIfProfile("mysql")
public @interface WithTimeZone {

String UTC = "UTC";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.client.UaaClientDetails;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.extensions.timezone.WithTimeZone;
import org.cloudfoundry.identity.uaa.extensions.database.DatabaseTestUTC;
import org.cloudfoundry.identity.uaa.oauth.common.exceptions.InvalidGrantException;
import org.cloudfoundry.identity.uaa.oauth.common.util.OAuth2Utils;
import org.cloudfoundry.identity.uaa.oauth.provider.OAuth2Authentication;
Expand Down Expand Up @@ -140,7 +140,7 @@ void deserializationOfUaaAuthentication() {
}

@Test
@WithTimeZone(WithTimeZone.UTC)
@DatabaseTestUTC
void consumeClientCredentialsFromOldStore() {
String code = legacyCodeServices.createAuthorizationCode(clientAuthentication);
assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM oauth_code WHERE code = ?", new Object[]{code}, Integer.class), is(1));
Expand Down

0 comments on commit a5a0075

Please sign in to comment.