Support setting time offset on test server within tests (#35495)

Closes: #34189

Signed-off-by: Simon Vacek <simonvacky@email.cz>
This commit is contained in:
Šimon Vacek 2024-12-04 12:09:46 +01:00 committed by GitHub
parent 806e140f45
commit 9abbf3edc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 483 additions and 2 deletions

View File

@ -87,6 +87,18 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-remote</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-remote-providers</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@ -21,6 +21,14 @@ public class KeycloakUrls {
return toUrl(getBase());
}
public String getMasterRealm() {
return baseUrl + "/realms/master";
}
public URL getMasterRealmUrl() {
return toUrl(getMasterRealm());
}
public String getAdmin() {
return baseUrl + "/admin";
}

View File

@ -80,6 +80,10 @@
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-ui</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-remote</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-example-providers</artifactId>

View File

@ -0,0 +1,29 @@
package org.keycloak.test.examples;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.test.framework.annotations.KeycloakIntegrationTest;
import org.keycloak.test.framework.remote.timeoffset.InjectTimeOffSet;
import org.keycloak.test.framework.remote.timeoffset.TimeOffSet;
@KeycloakIntegrationTest
public class TimeOffSetExampleTest {
@InjectTimeOffSet(offset = 3)
TimeOffSet timeOffSet;
@Test
public void testSetOffset() {
int offset = timeOffSet.get();
Assertions.assertEquals(3, offset);
Assertions.assertDoesNotThrow(() -> timeOffSet.set(10));
offset = timeOffSet.get();
Assertions.assertEquals(10, offset);
}
@Test
public void testGetOffset() {
int offset = timeOffSet.get();
Assertions.assertEquals(3, offset);
}
}

View File

@ -41,9 +41,11 @@
<module>db-oracle</module>
<module>db-postgres</module>
<module>email-server</module>
<module>oauth-nimbus-poc</module>
<module>ui</module>
<module>examples</module>
<module>oauth-nimbus-poc</module>
<module>remote</module>
<module>remote-providers</module>
<module>ui</module>
</modules>
</project>

View File

@ -0,0 +1,56 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-test-framework-parent</artifactId>
<groupId>org.keycloak.test</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-test-framework-remote-providers</artifactId>
<name>Keycloak Test Framework - Remote providers</name>
<packaging>jar</packaging>
<description>Utility Keycloak server Providers for the Keycloak Test Framework</description>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
package org.keycloak.test.framework.remote.providers;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.services.resource.RealmResourceProvider;
import java.util.Map;
public class TimeOffSetRealmResourceProvider implements RealmResourceProvider {
private final KeycloakSession session;
private final String KEY_OFFSET = "offset";
public TimeOffSetRealmResourceProvider(KeycloakSession session) {
this.session = session;
}
@Override
public Object getResource() {
return this;
}
@Override
public void close() {
}
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public Response getTimeOffset() {
int offset = Time.getOffset();
var time = Map.of(KEY_OFFSET, offset);
return Response.ok(time).build();
}
@PUT
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response setTimeOffset(Map<String, Integer> time) {
if (!time.containsKey(KEY_OFFSET)) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
Time.setOffset(time.get(KEY_OFFSET));
return Response.ok().header("Content-Type", MediaType.APPLICATION_JSON).build();
}
}

View File

@ -0,0 +1,36 @@
package org.keycloak.test.framework.remote.providers;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resource.RealmResourceProviderFactory;
public class TimeOffSetRealmResourceProviderFactory implements RealmResourceProviderFactory {
private final String ID = "testing-timeoffset";
@Override
public RealmResourceProvider create(KeycloakSession session) {
return new TimeOffSetRealmResourceProvider(session);
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
@Override
public void init(org.keycloak.Config.Scope config) {
}
}

View File

@ -0,0 +1 @@
org.keycloak.test.framework.remote.providers.TimeOffSetRealmResourceProviderFactory

View File

@ -0,0 +1,47 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-test-framework-parent</artifactId>
<groupId>org.keycloak.test</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-test-framework-remote</artifactId>
<name>Keycloak Test Framework - Remote testing utils</name>
<packaging>jar</packaging>
<description>Utility Keycloak server Suppliers for the Keycloak Test Framework</description>
<dependencies>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-framework-remote-providers</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,14 @@
package org.keycloak.test.framework.remote;
import org.keycloak.test.framework.injection.LifeCycle;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectRemoteProviders {
LifeCycle lifecycle() default LifeCycle.GLOBAL;
}

View File

@ -0,0 +1,4 @@
package org.keycloak.test.framework.remote;
public class RemoteProviders {
}

View File

@ -0,0 +1,48 @@
package org.keycloak.test.framework.remote;
import org.keycloak.test.framework.annotations.InjectTestDatabase;
import org.keycloak.test.framework.database.TestDatabase;
import org.keycloak.test.framework.injection.InstanceContext;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierOrder;
import org.keycloak.test.framework.server.KeycloakServerConfigBuilder;
import org.keycloak.test.framework.server.KeycloakServerConfigInterceptor;
public class RemoteProvidersSupplier implements Supplier<RemoteProviders, InjectRemoteProviders>, KeycloakServerConfigInterceptor<TestDatabase, InjectTestDatabase> {
@Override
public Class<InjectRemoteProviders> getAnnotationClass() {
return InjectRemoteProviders.class;
}
@Override
public Class<RemoteProviders> getValueType() {
return RemoteProviders.class;
}
@Override
public RemoteProviders getValue(InstanceContext<RemoteProviders, InjectRemoteProviders> instanceContext) {
return new RemoteProviders();
}
@Override
public LifeCycle getDefaultLifecycle() {
return LifeCycle.GLOBAL;
}
@Override
public boolean compatible(InstanceContext<RemoteProviders, InjectRemoteProviders> a, RequestedInstance<RemoteProviders, InjectRemoteProviders> b) {
return true;
}
@Override
public int order() {
return SupplierOrder.BEFORE_KEYCLOAK_SERVER;
}
@Override
public KeycloakServerConfigBuilder intercept(KeycloakServerConfigBuilder serverConfig, InstanceContext<TestDatabase, InjectTestDatabase> instanceContext) {
return serverConfig.dependency("org.keycloak.test", "keycloak-test-framework-remote-providers");
}
}

View File

@ -0,0 +1,17 @@
package org.keycloak.test.framework.remote;
import org.keycloak.test.framework.TestFrameworkExtension;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.remote.timeoffset.TimeOffsetSupplier;
import java.util.List;
public class RemoteTestFrameworkExtension implements TestFrameworkExtension {
@Override
public List<Supplier<?, ?>> suppliers() {
return List.of(
new TimeOffsetSupplier(),
new RemoteProvidersSupplier()
);
}
}

View File

@ -0,0 +1,17 @@
package org.keycloak.test.framework.remote.timeoffset;
import org.keycloak.test.framework.injection.LifeCycle;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectTimeOffSet {
LifeCycle lifecycle() default LifeCycle.METHOD;
int offset() default 0;
}

View File

@ -0,0 +1,65 @@
package org.keycloak.test.framework.remote.timeoffset;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.keycloak.common.util.Time;
import java.io.IOException;
import java.util.Map;
public class TimeOffSet {
private int currentOffset;
private final String KEY_OFFSET = "offset";
private final String TIME_OFFSET_ENDPOINT = "/testing-timeoffset";
private final HttpClient httpClient;
private final String serverUrl;
public TimeOffSet(HttpClient httpClient, String serverUrl, int initOffset) {
this.httpClient = httpClient;
this.serverUrl = serverUrl;
if (initOffset != 0) {
set(initOffset);
}
currentOffset = initOffset;
}
public void set(int offset) throws RuntimeException {
currentOffset = offset;
// set for tests
Time.setOffset(currentOffset);
// set for KC server
var time = Map.of(KEY_OFFSET, currentOffset);
try {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(time);
HttpPut request = new HttpPut(serverUrl + TIME_OFFSET_ENDPOINT);
request.setEntity(new StringEntity(json));
request.setHeader("Content-type", "application/json");
HttpResponse response = httpClient.execute(request);
if (response.getStatusLine().getStatusCode() != Response.Status.OK.getStatusCode()) {
var statusLine = response.getStatusLine();
throw new WebApplicationException(String.format("Unexpected response status for TimeOffSet: %d %s", statusLine.getStatusCode(), statusLine.getReasonPhrase()));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public int get() {
return currentOffset;
}
public boolean hasChanged() {
return currentOffset != 0;
}
}

View File

@ -0,0 +1,65 @@
package org.keycloak.test.framework.remote.timeoffset;
import org.apache.http.client.HttpClient;
import org.keycloak.test.framework.injection.InstanceContext;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierOrder;
import org.keycloak.test.framework.remote.RemoteProviders;
import org.keycloak.test.framework.server.KeycloakServerConfigBuilder;
import org.keycloak.test.framework.server.KeycloakServerConfigInterceptor;
import org.keycloak.test.framework.server.KeycloakUrls;
public class TimeOffsetSupplier implements Supplier<TimeOffSet, InjectTimeOffSet>, KeycloakServerConfigInterceptor<TimeOffSet, InjectTimeOffSet> {
@Override
public Class<InjectTimeOffSet> getAnnotationClass() {
return InjectTimeOffSet.class;
}
@Override
public Class<TimeOffSet> getValueType() {
return TimeOffSet.class;
}
@Override
public TimeOffSet getValue(InstanceContext<TimeOffSet, InjectTimeOffSet> instanceContext) {
var httpClient = instanceContext.getDependency(HttpClient.class);
var remoteProviders = instanceContext.getDependency(RemoteProviders.class);
KeycloakUrls keycloakUrls = instanceContext.getDependency(KeycloakUrls.class);
int initOffset = instanceContext.getAnnotation().offset();
return new TimeOffSet(httpClient, keycloakUrls.getMasterRealm(), initOffset);
}
@Override
public boolean compatible(InstanceContext<TimeOffSet, InjectTimeOffSet> a, RequestedInstance<TimeOffSet, InjectTimeOffSet> b) {
return true;
}
@Override
public LifeCycle getDefaultLifecycle() {
return LifeCycle.METHOD;
}
@Override
public void close(InstanceContext<TimeOffSet, InjectTimeOffSet> instanceContext) {
if (instanceContext.getLifeCycle() != LifeCycle.METHOD) {
TimeOffSet timeOffSet = instanceContext.getValue();
if (timeOffSet.hasChanged()) {
timeOffSet.set(0);
}
}
}
@Override
public int order() {
return SupplierOrder.BEFORE_KEYCLOAK_SERVER;
// Implementing the KeycloakServerConfigInterceptor is a workaround for RemoteProvidersSupplier to work
}
@Override
public KeycloakServerConfigBuilder intercept(KeycloakServerConfigBuilder serverConfig, InstanceContext<TimeOffSet, InjectTimeOffSet> instanceContext) {
return serverConfig;
}
}

View File

@ -0,0 +1 @@
org.keycloak.test.framework.remote.RemoteTestFrameworkExtension