Skip to content

Commit

Permalink
fix(#242): Support proxied and subclassed scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorsten Schlathoelter authored and bbortt committed Feb 15, 2024
1 parent cadb91b commit 6541364
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2006-2017 the original author or authors.
* Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,7 +21,6 @@
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.message.Message;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
import org.citrusframework.simulator.scenario.Scenario;
import org.citrusframework.simulator.scenario.ScenarioListAware;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.citrusframework.simulator.scenario.mapper.AbstractScenarioMapper;
Expand All @@ -33,12 +32,15 @@
import java.util.List;
import java.util.Optional;

import static org.citrusframework.simulator.scenario.ScenarioUtils.getAnnotationFromClassHierarchy;

/**
* Scenario mapper performs mapping logic on request mapping annotations on given scenarios. Scenarios match on request method as well as
* request path pattern matching.
*
* @author Christoph Deppisch
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public class HttpRequestAnnotationScenarioMapper extends AbstractScenarioMapper implements ScenarioListAware {

private final HttpRequestAnnotationMatcher httpRequestAnnotationMatcher = HttpRequestAnnotationMatcher.instance();
Expand All @@ -48,8 +50,8 @@ public class HttpRequestAnnotationScenarioMapper extends AbstractScenarioMapper

@Override
protected String getMappingKey(Message request) {
if (request instanceof HttpMessage) {
return getMappingKeyForHttpMessage((HttpMessage) request);
if (request instanceof HttpMessage httpMessage) {
return getMappingKeyForHttpMessage(httpMessage);
}

return super.getMappingKey(request);
Expand All @@ -62,7 +64,7 @@ protected String getMappingKeyForHttpMessage(HttpMessage httpMessage) {
Optional<String> mapping = nullSafeList.stream()
.map(scenario -> EnrichedScenarioWithRequestMapping.builder()
.scenario(scenario)
.requestMapping(AnnotationUtils.findAnnotation(scenario.getClass(), RequestMapping.class))
.requestMapping(getAnnotationFromClassHierarchy(scenario, RequestMapping.class))
.build()
)
.filter(EnrichedScenarioWithRequestMapping::hasRequestMapping)
Expand Down Expand Up @@ -134,12 +136,13 @@ public void setConfiguration(SimulatorConfigurationProperties configuration) {

@Builder
private record EnrichedScenarioWithRequestMapping(SimulatorScenario scenario, RequestMapping requestMapping) {

public boolean hasRequestMapping() {
return requestMapping != null;
}

public String name() {
return scenario.getClass().getAnnotation(Scenario.class).value();
return scenario.getName();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2024 the original author or authors.
*
* 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.
*/

package org.citrusframework.simulator.scenario;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.NoArgsConstructor;

import java.lang.annotation.Annotation;

import static lombok.AccessLevel.PRIVATE;
import static org.springframework.aop.framework.AopProxyUtils.ultimateTargetClass;

@NoArgsConstructor(access = PRIVATE)
public class ScenarioUtils {

/**
* Retrieves the specified annotation from the class hierarchy of the given scenario object.
* If the scenario object is a proxy, this method unwraps the proxy to obtain the actual target class.
*
* @param scenario The scenario object to search for the annotation. Must not be null.
* @param annotationType The type of annotation to retrieve.
* @param <T> The type of the annotation.
* @return The annotation if found, otherwise {@code null}.
*/
@Nullable
public static <T extends Annotation> T getAnnotationFromClassHierarchy(@Nonnull SimulatorScenario scenario, Class<T> annotationType) {
return getAnnotationFromClassHierarchy(ultimateTargetClass(scenario), annotationType);
}

/**
* Retrieves the specified annotation from the class hierarchy of the given scenario class.
*
* @param scenarioClass The class to search for the annotation.
* @param annotationType The type of annotation to retrieve.
* @param <T> The type of the annotation.
* @return The annotation if found, otherwise {@code null}.
*/
@Nullable
public static <T extends Annotation> T getAnnotationFromClassHierarchy(@Nonnull Class<?> scenarioClass, Class<T> annotationType) {
T annotation = scenarioClass.getAnnotation(annotationType);
if (annotation != null) {
return annotation;
} else if (scenarioClass.getSuperclass() != null) {
return getAnnotationFromClassHierarchy(scenarioClass.getSuperclass(), annotationType);
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2006-2017 the original author or authors.
* Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,20 +16,46 @@

package org.citrusframework.simulator.scenario;

import org.citrusframework.exceptions.CitrusRuntimeException;

import static java.lang.String.format;
import static org.citrusframework.simulator.scenario.ScenarioUtils.getAnnotationFromClassHierarchy;

/**
* @author Christoph Deppisch
*/
public interface SimulatorScenario {

/**
* Gets the scenario endpoint explicitly set to handle messages for this scenario.
* @return
*/
ScenarioEndpoint getScenarioEndpoint();

/**
* Default starter body method with provided scenario runner.
* @param runner
*/
default void run(ScenarioRunner runner) {}
default void run(ScenarioRunner runner) {
}

default String getName() {
return getNameFromScenarioAnnotation();
}

/**
* Retrieves the name of a scenario from its {@link Scenario} annotation.
*
* @return the name of the scenario as specified by the {@link Scenario} annotation's value.
* @throws CitrusRuntimeException if the {@link Scenario} annotation is not found on this
* scenario, its proxied objects or superclasses.
*/
private String getNameFromScenarioAnnotation() {
Scenario scenarioAnnotation = getAnnotationFromClassHierarchy(this, Scenario.class);

if (scenarioAnnotation == null) {
throw new CitrusRuntimeException(
format("Missing scenario annotation at class: %s - even searched class hierarchy", getClass())
);
}
return scenarioAnnotation.value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2024 the original author or authors.
*
* 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.
*/

package org.citrusframework.simulator.http;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
import org.citrusframework.simulator.http.HttpRequestAnnotationScenarioMapperIT.AspectTestConfiguration;
import org.citrusframework.simulator.scenario.AbstractSimulatorScenario;
import org.citrusframework.simulator.scenario.Scenario;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.ArrayList;
import java.util.Collection;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.http.HttpMethod.PUT;

/**
* @author Thorsten Schlathoelter
*/
@ExtendWith(SpringExtension.class)
@Import(AspectTestConfiguration.class)
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
class HttpRequestAnnotationScenarioMapperIT {

@Autowired
private ApplicationContext applicationContext;

@Autowired
private HttpRequestAnnotationScenarioMapper fixture;

@Test
void testGetMappingKeyFromProxiedScenario() {
Collection<SimulatorScenario> scenarios = applicationContext.getBeansOfType(SimulatorScenario.class).values();

assertThat(scenarios).hasSize(1);

assertThat(AopUtils.isAopProxy(scenarios.iterator().next())).isTrue();
fixture.setScenarioList(new ArrayList<>(scenarios));
assertEquals("PutFooScenario", fixture.getMappingKey(new HttpMessage().path("/issues/foo").method(PUT)));
}

@Scenario("PutFooScenario")
@RequestMapping(value = "/issues/foo", method = RequestMethod.PUT)
public static class PutFooScenario extends AbstractSimulatorScenario {
}

@TestConfiguration
@EnableAspectJAutoProxy
public static class AspectTestConfiguration {

@Bean
public SimulatorScenario putFooScenario() {
return new PutFooScenario();
}

@Bean
public ScenarioWrappingAspect myAspect() {
return new ScenarioWrappingAspect();
}

@Bean
public HttpRequestAnnotationScenarioMapper httpRequestAnnotationScenarioMapper() {
return new HttpRequestAnnotationScenarioMapper();
}

@Bean
public SimulatorConfigurationProperties simulatorConfigurationProperties() {
return new SimulatorConfigurationProperties();
}
}

@Aspect
public static class ScenarioWrappingAspect {

private static final String RUN_SCENARIO_POINTCUT =
"within(org.citrusframework.simulator.scenario.SimulatorScenario+) && execution(* run(org.citrusframework.simulator.scenario.ScenarioRunner))";

@Around(RUN_SCENARIO_POINTCUT)
public Object interceptScenarioExecution(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
/*
* Copyright 2024 the original author or authors.
*
* 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.
*/

package org.citrusframework.simulator.http;

import jakarta.annotation.Nonnull;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
Expand All @@ -19,6 +36,10 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doReturn;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;

/**
* @author Christoph Deppisch
Expand All @@ -43,32 +64,47 @@ void beforeEachSetup() {
void testGetMappingKey() {
fixture.setScenarioList(Arrays.asList(new IssueScenario(),
new FooScenario(),
new SubclassedFooScenario(),
new GetFooScenario(),
new PutFooScenario(),
new OtherScenario()));

assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/foo")), "FooScenario");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/foo").method(HttpMethod.GET)), "GetFooScenario");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/foo").method(HttpMethod.PUT)), "PutFooScenario");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/other")), "OtherScenario");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/bar").method(HttpMethod.GET)), "IssueScenario");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/bar").method(HttpMethod.DELETE)), "IssueScenario");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/bar").method(HttpMethod.PUT)), "default");
assertEquals(fixture.getMappingKey(new HttpMessage().path("/issues/bar")), "default");
assertEquals(fixture.getMappingKey(null), "default");
assertEquals("FooScenario", mappingKeyFor(fixture, "/issues/foo"));
assertEquals("GetFooScenario", mappingKeyFor(fixture, "/issues/foo", GET));
assertEquals("PutFooScenario", mappingKeyFor(fixture, "/issues/foo",PUT));
assertEquals("FooScenario", mappingKeyFor(fixture, "/issues/foo/sub", POST));
assertEquals("OtherScenario", mappingKeyFor(fixture, "/issues/other"));
assertEquals("IssueScenario", mappingKeyFor(fixture, "/issues/bar", GET));
assertEquals("IssueScenario", mappingKeyFor(fixture, "/issues/bar", DELETE));
assertEquals("default", mappingKeyFor(fixture, "/issues/bar", PUT));
assertEquals("default", mappingKeyFor(fixture, "/issues/bar"));
assertEquals("default", fixture.getMappingKey(null));

fixture.setUseDefaultMapping(false);

assertThrows(CitrusRuntimeException.class, () -> fixture.getMappingKey(new HttpMessage().path("/issues/bar").method(HttpMethod.PUT)));
assertThrows(CitrusRuntimeException.class, () -> fixture.getMappingKey(new HttpMessage().path("/issues/bar")));
assertThrows(CitrusRuntimeException.class, () -> mappingKeyFor(fixture, "/issues/bar", PUT));
assertThrows(CitrusRuntimeException.class, () -> mappingKeyFor(fixture, "/issues/bar"));
assertThrows(CitrusRuntimeException.class, () -> fixture.getMappingKey(null));
}


private String mappingKeyFor(HttpRequestAnnotationScenarioMapper mapper, String path) {
return mapper.getMappingKey(new HttpMessage().path(path));
}

private String mappingKeyFor(HttpRequestAnnotationScenarioMapper mapper, String path, @Nonnull HttpMethod method) {
return mapper.getMappingKey(new HttpMessage().path(path).method(method));
}

@Scenario("FooScenario")
@RequestMapping(value = "/issues/foo", method = RequestMethod.POST)
private static class FooScenario extends AbstractSimulatorScenario {
}

@RequestMapping(value = "/issues/foo/sub", method = RequestMethod.POST)
private static class SubclassedFooScenario extends FooScenario {
}

@Scenario("GetFooScenario")
@RequestMapping(value = "/issues/foo", method = RequestMethod.GET)
private static class GetFooScenario extends AbstractSimulatorScenario {
Expand Down

0 comments on commit 6541364

Please sign in to comment.