Skip to content

Commit

Permalink
feat(#1083): Add OpenAPI connector
Browse files Browse the repository at this point in the history
- Connector implementation adding OpenAPI client and server support
- Generates request/response message data based on given OpenAPI specification
- Generates validation control message content based on the specification
- Users reference operations by their id
- Responses get identified by the Http status code as defined in the specification
- Validation/generation also includes Content-Type and resource URL path settings
- Supports path parameters and random test data
  • Loading branch information
christophd committed Dec 12, 2023
1 parent 6d65427 commit d373c81
Show file tree
Hide file tree
Showing 102 changed files with 7,729 additions and 201 deletions.
5 changes: 5 additions & 0 deletions catalog/citrus-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@
<artifactId>citrus-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-openapi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-jms</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import org.citrusframework.context.TestContext;
import org.citrusframework.context.TestContextFactory;
import org.citrusframework.functions.DefaultFunctionLibrary;
import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary;
import org.testng.annotations.BeforeMethod;

/**
Expand All @@ -24,9 +22,6 @@ public void prepareTest() {
}

protected TestContextFactory createTestContextFactory() {
TestContextFactory factory = TestContextFactory.newInstance();
factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary());
factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary());
return factory;
return TestContextFactory.newInstance();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import org.citrusframework.context.TestContext;
import org.citrusframework.context.TestContextFactory;
import org.citrusframework.functions.DefaultFunctionLibrary;
import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary;
import org.testng.annotations.BeforeMethod;

/**
Expand All @@ -24,9 +22,6 @@ public void prepareTest() {
}

protected TestContextFactory createTestContextFactory() {
TestContextFactory factory = TestContextFactory.newInstance();
factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary());
factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary());
return factory;
return TestContextFactory.newInstance();
}
}
104 changes: 104 additions & 0 deletions connectors/citrus-openapi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>citrus-connectors</artifactId>
<groupId>org.citrusframework</groupId>
<version>4.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>citrus-openapi</artifactId>
<name>Citrus :: Connectors :: OpenAPI</name>

<dependencies>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-spring</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-xml</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-yaml</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.apicurio</groupId>
<artifactId>apicurio-data-models</artifactId>
</dependency>

<!-- Test scoped dependencies -->
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-test-support</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-testng</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-groovy</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-validation-json</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
<profile>
<id>schemagen</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>schemagen</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<sources>
<source>src/main/java/org/citrusframework/openapi/xml/Openapi.java</source>
</sources>
<outputDirectory>${project.build.directory}/schemas</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.openapi;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;

import com.fasterxml.jackson.databind.JsonNode;
import io.apicurio.datamodels.Library;
import io.apicurio.datamodels.openapi.models.OasDocument;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.ssl.SSLContexts;
import org.citrusframework.spi.Resource;
import org.citrusframework.util.FileUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;

/**
* Loads Open API specifications from different locations like file resource or web resource.
* @author Christoph Deppisch
*/
public final class OpenApiResourceLoader {

/**
* Prevent instantiation of utility class.
*/
private OpenApiResourceLoader() {
super();
}

/**
* Loads the specification from a file resource. Either classpath or file system resource path is supported.
* @param resource
* @return
*/
public static OasDocument fromFile(String resource) {
return fromFile(FileUtils.getFileResource(resource));
}

/**
* Loads the specification from a file resource. Either classpath or file system resource path is supported.
* @param resource
* @return
*/
public static OasDocument fromFile(Resource resource) {
try {
return resolve(FileUtils.readToString(resource));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse Open API specification: " + resource, e);
}
}

/**
* Loads specification from given web URL location.
* @param url
* @return
*/
public static OasDocument fromWebResource(URL url) {
HttpURLConnection con = null;
try {
con = (HttpURLConnection) url.openConnection();
con.setRequestMethod(HttpMethod.GET.name());
con.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);

int status = con.getResponseCode();
if (status > 299) {
throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(),
new IOException(FileUtils.readToString(con.getErrorStream())));
} else {
return resolve(FileUtils.readToString(con.getInputStream()));
}
} catch (IOException e) {
throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), e);
} finally {
if (con != null) {
con.disconnect();
}
}
}

/**
* Loads specification from given web URL location using secured Http connection.
* @param url
* @return
*/
public static OasDocument fromSecuredWebResource(URL url) {
Objects.requireNonNull(url);

HttpsURLConnection con = null;
try {
SSLContext sslcontext = SSLContexts
.custom()
.loadTrustMaterial(TrustAllStrategy.INSTANCE)
.build();

HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE);

con = (HttpsURLConnection) url.openConnection();
con.setRequestMethod(HttpMethod.GET.name());
con.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);

int status = con.getResponseCode();
if (status > 299) {
throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(),
new IOException(FileUtils.readToString(con.getErrorStream())));
} else {
return resolve(FileUtils.readToString(con.getInputStream()));
}
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
throw new IllegalStateException("Failed to create https client for ssl connection", e);
} catch (IOException e) {
throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), e);
} finally {
if (con != null) {
con.disconnect();
}
}
}

private static OasDocument resolve(String specification) {
if (isJsonSpec(specification)) {
return (OasDocument) Library.readDocumentFromJSONString(specification);
}

final JsonNode node = OpenApiSupport.json().convertValue(OpenApiSupport.yaml().load(specification), JsonNode.class);
return (OasDocument) Library.readDocument(node);
}

private static boolean isJsonSpec(final String specification) {
return specification.trim().startsWith("{");
}
}
Loading

0 comments on commit d373c81

Please sign in to comment.