Skip to content

Commit

Permalink
feat(#285): support OpenAPI 3.0 from HttpOperationScenario
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorsten Schlathoelter committed Jan 10, 2025
1 parent ce8911e commit fc9d16e
Show file tree
Hide file tree
Showing 40 changed files with 3,793 additions and 813 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -526,4 +526,5 @@
</profile>

</profiles>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.testng.annotations.Ignore;
import org.testng.annotations.Test;

import static org.citrusframework.actions.SendMessageAction.Builder.send;
Expand All @@ -37,6 +38,7 @@
* @author Christoph Deppisch
*/
@Test
@Ignore
@ContextConfiguration(classes = SimulatorMailIT.EndpointConfig.class)
public class SimulatorMailIT extends TestNGCitrusSpringSupport {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
import org.citrusframework.endpoint.adapter.StaticEndpointAdapter;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.message.Message;
import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
import org.citrusframework.simulator.scenario.mapper.ScenarioMappers;
import org.citrusframework.openapi.OpenApiRepository;
import org.citrusframework.simulator.http.HttpRequestAnnotationScenarioMapper;
import org.citrusframework.simulator.http.HttpRequestPathScenarioMapper;
import org.citrusframework.simulator.http.HttpResponseActionBuilderProvider;
import org.citrusframework.simulator.http.HttpScenarioGenerator;
import org.citrusframework.simulator.http.SimulatorRestAdapter;
import org.citrusframework.simulator.http.SimulatorRestConfigurationProperties;
import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
import org.citrusframework.simulator.scenario.mapper.ScenarioMappers;
import org.citrusframework.spi.Resources;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand All @@ -47,12 +49,13 @@ public static void main(String[] args) {
@Override
public ScenarioMapper scenarioMapper() {
return ScenarioMappers.of(new HttpRequestPathScenarioMapper(),
new HttpRequestAnnotationScenarioMapper());
new HttpRequestAnnotationScenarioMapper());
}

@Override
public List<String> urlMappings(SimulatorRestConfigurationProperties simulatorRestConfiguration) {
return List.of("/petstore/v2/**");
public List<String> urlMappings(
SimulatorRestConfigurationProperties simulatorRestConfiguration) {
return List.of("/petstore/v2/**", "/petstore/api/v3/**", "/pingapi/v1/**");
}

@Override
Expand All @@ -67,8 +70,36 @@ protected Message handleMessageInternal(Message message) {

@Bean
public static HttpScenarioGenerator scenarioGenerator() {
HttpScenarioGenerator generator = new HttpScenarioGenerator(new Resources.ClasspathResource("swagger/petstore-api.json"));
generator.setContextPath("/petstore");
return generator;
return new HttpScenarioGenerator(
Resources.create("classpath:swagger/petstore-api.json"));
}

@Bean
public static OpenApiRepository swaggerRepository() {
OpenApiRepository openApiRepository = new OpenApiRepository();
openApiRepository.setRootContextPath("/petstore");
openApiRepository.setLocations(List.of("swagger/petstore-api.json"));
return openApiRepository;
}

@Bean
public static OpenApiRepository openApiRepository() {
OpenApiRepository openApiRepository = new OpenApiRepository();
openApiRepository.setRootContextPath("/petstore");
openApiRepository.setLocations(List.of("openapi/petstore-v3.json"));
return openApiRepository;
}

@Bean
public static OpenApiRepository pingApiRepository() {
OpenApiRepository openApiRepository = new OpenApiRepository();
openApiRepository.setLocations(List.of("openapi/ping-v1.yaml"));
return openApiRepository;
}

@Bean
static HttpResponseActionBuilderProvider httpResponseActionBuilderProvider() {
return new SpecificPingResponseMessageBuilder();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.citrusframework.simulator.sample;

import static java.lang.String.format;
import static org.citrusframework.openapi.OpenApiSettings.getResponseAutoFillRandomValues;

import io.apicurio.datamodels.openapi.models.OasOperation;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.function.TriFunction;
import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.http.message.HttpMessageHeaders;
import org.citrusframework.message.MessageType;
import org.citrusframework.openapi.actions.OpenApiActionBuilder;
import org.citrusframework.openapi.actions.OpenApiServerActionBuilder;
import org.citrusframework.openapi.actions.OpenApiServerResponseActionBuilder;
import org.citrusframework.simulator.http.HttpOperationScenario;
import org.citrusframework.simulator.http.HttpResponseActionBuilderProvider;
import org.citrusframework.simulator.scenario.ScenarioRunner;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.springframework.http.MediaType;

/**
* {@link HttpResponseActionBuilderProvider} that provides specific responses for dedicated ping
* calls. Shows, how to use a {@link HttpResponseActionBuilderProvider} to control the random
* message generation.
*/
public class SpecificPingResponseMessageBuilder implements HttpResponseActionBuilderProvider {

private static final int MISSING_ID = Integer.MIN_VALUE;

/**
* Function that returns null to indicate, that the provider does not provide a builder for the given scenario.
*/
private static final TriFunction<OpenApiServerActionBuilder, OasOperation, HttpMessage, HttpServerResponseActionBuilder> NULL_RESPONSE = SpecificPingResponseMessageBuilder::createNull;

/**
* Map to store specific functions per ping id.
*/
private static final Map<Integer, TriFunction<OpenApiServerActionBuilder, OasOperation, HttpMessage, HttpServerResponseActionBuilder>> SPECIFC_BUILDER_MAP = new HashMap<>();

// Specific responses for some ids, all others will be handled by returning null and letting the random generator do its work.
static {
SPECIFC_BUILDER_MAP.put(15000,
SpecificPingResponseMessageBuilder::createResponseWithDedicatedRequiredHeader);
SPECIFC_BUILDER_MAP.put(10000,
SpecificPingResponseMessageBuilder::createResponseWithMessageAndHeaders);
SPECIFC_BUILDER_MAP.put(5000, SpecificPingResponseMessageBuilder::createResponseWithSpecificBody);
SPECIFC_BUILDER_MAP.put(4000,
SpecificPingResponseMessageBuilder::createResponseWithRandomGenerationSuppressed);
}

@Override
public HttpServerResponseActionBuilder provideHttpServerResponseActionBuilder(
ScenarioRunner scenarioRunner, SimulatorScenario simulatorScenario,
HttpMessage receivedMessage) {

if (!(simulatorScenario instanceof HttpOperationScenario httpOperationScenario)) {
return null;
}

OpenApiServerActionBuilder openApiServerActionBuilder = new OpenApiActionBuilder(
httpOperationScenario.getOpenApiSpecification()).server(scenarioRunner.getScenarioEndpoint());

return SPECIFC_BUILDER_MAP.getOrDefault(getIdFromPingRequest(receivedMessage), NULL_RESPONSE).apply(openApiServerActionBuilder, httpOperationScenario.getOperation(), receivedMessage);
}

private static Integer getIdFromPingRequest(HttpMessage httpMessage) {
String uri = httpMessage.getUri();
Pattern pattern = Pattern.compile("/pingapi/v1/ping/(\\d*)");
Matcher matcher = pattern.matcher(uri);
if (matcher.matches()) {
return Integer.parseInt(matcher.group(1));
}
return MISSING_ID;
}

/**
* Sample to prove, that random data generation can be suppressed. Note that the generated
* response is thus invalid and will result in an error.
*/
private static OpenApiServerResponseActionBuilder createResponseWithRandomGenerationSuppressed(
OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
HttpMessage receivedMessage) {
OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
oasOperation.operationId, "200").enableRandomGeneration(getResponseAutoFillRandomValues());
sendMessageBuilder.message().body(format("{\"id\": %d, \"pingTime\": %d}",
getIdFromPingRequest(receivedMessage), System.currentTimeMillis()));
return sendMessageBuilder;
}

/**
* Sample to prove, that the body content can be controlled, while headers will be generated by
* random generator.
*/
private static OpenApiServerResponseActionBuilder createResponseWithSpecificBody(
OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
HttpMessage receivedMessage) {
OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
oasOperation.operationId, "200");
sendMessageBuilder.message().body(format("{\"id\": %d, \"pingCount\": %d}",
getIdFromPingRequest(receivedMessage), System.currentTimeMillis()));
return sendMessageBuilder;
}

/**
* Sample to prove, that the status, response and headers can be controlled and are not
* overwritten by random generator.
*/
private static OpenApiServerResponseActionBuilder createResponseWithMessageAndHeaders(
OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
HttpMessage receivedMessage) {
OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
oasOperation.operationId, "400", receivedMessage.getAccept());
sendMessageBuilder.message().type(MessageType.PLAINTEXT)
.header(HttpMessageHeaders.HTTP_CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.header("Ping-Time", "1").body("Requests with id == 10000 cannot be processed!");
return sendMessageBuilder;
}

/**
* Sample to prove, that a preset header can be controlled, while generating a valid random
* response.
*/
private static OpenApiServerResponseActionBuilder createResponseWithDedicatedRequiredHeader(
OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
HttpMessage receivedMessage) {
OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
oasOperation.operationId, "200", receivedMessage.getAccept());
sendMessageBuilder.message().header("Ping-Time", "0");
return sendMessageBuilder;
}

private static OpenApiServerResponseActionBuilder createNull(
OpenApiServerActionBuilder ignoreOpenApiServerActionBuilder,
OasOperation ignoreOasOperation, HttpMessage ignoreReceivedMessage) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Note, that the petstore-v3.json has been slightly modified from its original version.
OK messages have been added, where missing, to be able to activate the response validation feature.
Loading

0 comments on commit fc9d16e

Please sign in to comment.