Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSUE-195 # Generation of Scenarios from OpenAPI specifications (POC) #695

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<module>kafka-testing</module>
<module>junit5-testing</module>
<module>zerocode-maven-archetype</module>
<module>zerocode-openapi</module>
</modules>

<developers>
Expand Down
55 changes: 55 additions & 0 deletions zerocode-openapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Zerocode OpenAPI Generator (POC)

This module (`zerocode-openapi`) is a proof of concept (POC) of a generator of Zerocode scenarios from [OpenAPI](https://swagger.io/docs/specification/v3_0/about/) specifications.

This generator provides a single entry point `generateAll`
in the `or.jsmart.zerocode.openapi.ScenarioGenerator` class, that takes two arguments:
- File name or url where the OpenAPI specification is located.
- A folder where the generated scenarios will be placed.

The output is a set of scenarios (one for each path in the OpenAPI specification), each containing a step for each operation. Scenario files take the name of the path. Symbols and non ascii characters are replaced by underscore, except brackets (used for path parameters)

## Tests

- Unit Tests: Exercise simple scenarios. Result comparison is made between the generated scenarios (in target) and the expected scenarios (in resources). Html files with the differences are stored in the target folder for offline checking.
- Integration Test: Scenarios generated from the [Swagger Petstore](https://github.com/swagger-api/swagger-petstore) specification, that is the demonstrator of this POC. Result comparison is made as in unit tests using soft assertions. An additional manual performance test was made using the OpenAPI specification of the GitHub API (631 scenarios).
- End to End Tests: Manual tests to verify that the generated scenarios can run against the real Swagger Petstore backend. Test scripts are provided in the E2eTest class.

## Supported features

Currently supported features are enumerated below in the order stated in https://swagger.io/docs/specification/v3_0/about/:

- Media Types:
- The schema of the content is the one defined by the `application/json media` type (if present).
- If not, the first media type found is used, and the corresponding `Content-Type` header is set.
- Operations: Each scenario can generate steps for the `POST` `GET` `PUT` `PATCH` `HEAD` `OPTIONS` `TRACE` and `DELETE` operations defined in the path.
- Parameters and Serialization:
- Primitive Query Parameters: Added to `request.queryParameters`.
- Array Query Parameters: Serialized and url encoded in the path. Supports:
- `form` (default), `spaceDelimited` and `pipeDelimited` formats.
- `explode` (true by default).
- Primitive Path Parameters: Serialized and url encoded in the path.
- Primitive Header Parameters: Added to `request.headers`, `explode` is not supported
- Request Body: As indicated in Media Types.
- Responses: Generates an assertion for the first `2xx` response, if any. Response content is ignored.
- Data Models: Primitive values are generated by Zerocode tokens unless explicitly indicated:
- Primitive data Types: `string`, `number`, `integer`, `boolean` (randomly generated).
- Formats: String `date` and `date-time`.
- Enums: Of primitive data. Values are randomly generated.
- Arrays and Objects: as defined by their schemas.
- Maps: The `additionalProperties` keyword generates maps of primitive and non primitive. Free-Form is not supported
- References: `$ref` is handled by the swagger parser.

## Zerocode enhancements

All scenarios generated for the swagger petstore API generate valid requests, but there are a number of possible improvements to enhance the data generation:

- *[Bug] Random strings are not random in a step*. All values generated by the RANDOM.STRING token have the same value inside each step. As workaround, a random number is appended to the random string.
- *Add a token to generate a random value among a set of values*. The ONE.OF token is only supported in asserts. If implemented to generate values in the request body, it would be used to generate enums and booleans without needing random generator at the time of writing the scenarios. Note that this should be able to generate both strings and non strings (quoted and unquoted values, including boolean).
- *Send unquoted numbers in requests*. The value generated for random numbers is a string. This could cause problems problems in the backend to accept these values depending on its serialization approach.
- *Add tokens to generate random date and datetime*. Currently the date/time values generate the current date, that leads to repeated values in a step.
- *Support Zulu timezone designator when generating dates*. Step execution fails if Z is included into the datetime format (it also fails with milliseconds).
- *Add tokens to generate numbers with decimals*. Current workaround concatenates two numbers and dot to include decimals, but this is sent as a string, not as a number
- *Allow ONE.OF to check the response status*. Lowest priority as having more than one 2xx possible responses should be infrequent. Currently the generators adds an assertion for the first success response code that is found.


56 changes: 56 additions & 0 deletions zerocode-openapi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>zerocode-tdd-parent</artifactId>
<groupId>org.jsmart</groupId>
<version>1.3.45-SNAPSHOT</version>
</parent>

<artifactId>zerocode-openapi</artifactId>

<packaging>jar</packaging>
<name>POC - Zerocode test generation from OpenAPI specifications</name>
<description>Proof of concept - Generation of Zerocde scenarios and steps from the paths and operations of an OpenAPI specification</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<artifactId>zerocode-tdd</artifactId>
<groupId>org.jsmart</groupId>
<version>${project.version}</version>
</dependency>
<!-- TODO move version/scopes to the parent pom -->
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.23</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>io.github.javiertuya</groupId>
<artifactId>visual-assert</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.jsmart.zerocode.openapi;

import org.jsmart.zerocode.openapi.types.DataGenerator;
import org.jsmart.zerocode.openapi.types.ArrayGenerator;
import org.jsmart.zerocode.openapi.types.BooleanGenerator;
import org.jsmart.zerocode.openapi.types.IntegerGenerator;
import org.jsmart.zerocode.openapi.types.NumberGenerator;
import org.jsmart.zerocode.openapi.types.ObjectGenerator;
import org.jsmart.zerocode.openapi.types.StringGenerator;

import io.swagger.v3.oas.models.media.Schema;

public class DataGeneratorFactory implements IDataGeneratorFactory {

public DataGenerator getItem(String name, Schema<?> schema) {
if ("integer".equals(schema.getType())) {
return new IntegerGenerator(name, schema);
} else if ("number".equals(schema.getType())) {
return new NumberGenerator(name, schema);
} else if ("string".equals(schema.getType())) {
return new StringGenerator(name, schema);
} else if ("boolean".equals(schema.getType())) {
return new BooleanGenerator(name, schema);
} else if ("array".equals(schema.getType())) {
return new ArrayGenerator(name, schema, this); // requires factory to create objects
} else if ("object".equals(schema.getType())) {
return new ObjectGenerator(name, schema, this);
}
throw new RuntimeException(
String.format("OpenAPI schema type %s not allowed, property: %s", schema.getType(), name));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.jsmart.zerocode.openapi;

import org.jsmart.zerocode.openapi.types.DataGenerator;

import io.swagger.v3.oas.models.media.Schema;

public interface IDataGeneratorFactory {

DataGenerator getItem(String name, Schema<?> schema);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.jsmart.zerocode.openapi;

import static org.slf4j.LoggerFactory.getLogger;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.jsmart.zerocode.openapi.types.DataGenerator;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.swagger.v3.oas.models.parameters.Parameter;

/**
* OpenAPI supports parameters at several places
* https://swagger.io/docs/specification/v3_0/describing-parameters/
* Currently: query string, path and header parameters are supported (no cookie params)
*
* There are multiple formats for manage non primitive values:
* https://swagger.io/docs/specification/v3_0/serialization/
* Currently: the supported parameters are
* - path: primitive
* - query string: primitive, array (Styles delimited by comma, space and pipe, others ignored)
* - headers: primitive
*
* Error handling: Warn and ignore if a data type or style is not supported
*/
public class ParameterSerializer {

private static final org.slf4j.Logger LOGGER = getLogger(ParameterSerializer.class);

public String serializePathParams(String path, List<Parameter> oaParams) {
for (Parameter oaParam : filterParams(oaParams, "path")) {
if (!isObject(oaParam) && !isArray(oaParam)) {
DataGeneratorFactory factory = new DataGeneratorFactory();
JsonNode value = factory.getItem(oaParam.getName(), oaParam.getSchema()).setRequireUrlEncode(true).generateJsonValue();
path = path.replace("{" + oaParam.getName() + "}", value.asText());
} else {
LOGGER.warn("Non primitive path parameters are not supported yet");
}
}
return path;
}

public JsonNode getQueryParams(List<Parameter> oaParams) {
ObjectNode params = new ObjectMapper().createObjectNode();
for (Parameter oaParam : filterParams(oaParams, "query")) {
// Only primitive params are in the header, other types are set in the path
if (!isObject(oaParam) && !isArray(oaParam)) {
DataGeneratorFactory factory = new DataGeneratorFactory();
JsonNode value = factory.getItem(oaParam.getName(), oaParam.getSchema()).generateJsonValue();
params.set(oaParam.getName(), value);
}
}
return params;
}

public String serializeQueryParams(String path, List<Parameter> oaParams) {
List<String> queryString = new ArrayList<>();
for (Parameter oaParam : filterParams(oaParams, "query")) {
if (isArray(oaParam)) {
String params = getArrayQueryParams(oaParam);
if (params != null)
queryString.add(params);
} else if (isObject(oaParam)) {
LOGGER.warn("Object query parameters are not supported yet");
} // primitive are not serialized in the path
}
// For now, generation is quite simple, not using any url template library
// Assuming the OpenAPI spec has no query strings in the url
if (!queryString.isEmpty())
path = path + "?" + String.join("&", queryString);
return path;
}

private String getArrayQueryParams(Parameter oaParam) {
DataGeneratorFactory factory = new DataGeneratorFactory();
DataGenerator generator = factory.getItem(oaParam.getName(), oaParam.getSchema()).setRequireUrlEncode(true);
String paramName = generator.encodeIfRequired(oaParam.getName()); // values will be encoded when generated
String arraySeparator = getArraySeparator(oaParam.getStyle());
if (arraySeparator == null) {
LOGGER.warn("Array query parameter style {} is not supported yet", oaParam.getStyle().toString());
return null; // to not add any parameter
}

JsonNode jsonArray = generator.generateJsonValue();
List<String> items = new ArrayList<>();
for (JsonNode value : jsonArray) {
if (oaParam.getExplode())
items.add(paramName + "=" + value.textValue());
else
items.add(value.textValue());
}
if (oaParam.getExplode())
return String.join("&", items);
else
return paramName + "=" + String.join(arraySeparator, items);
}

private String getArraySeparator(Parameter.StyleEnum style) {
if (style == Parameter.StyleEnum.FORM)
return ",";
else if (style == Parameter.StyleEnum.SPACEDELIMITED)
return "%20";
else if (style == Parameter.StyleEnum.PIPEDELIMITED)
return "|";
else
return null;
}

public JsonNode getHeaderParams(List<Parameter> oaParams) {
ObjectNode params = new ObjectMapper().createObjectNode();
for (Parameter oaParam : filterParams(oaParams, "header")) {
// Specification has only the "simple" style, no considering explode
// If considering explode eventually, refactor with getArrayQueryParams
DataGeneratorFactory factory = new DataGeneratorFactory();
JsonNode value = factory.getItem(oaParam.getName(), oaParam.getSchema()).generateJsonValue();
params.set(oaParam.getName(), value);
}
return params;
}

private List<Parameter> filterParams(List<Parameter> oaParams, String in) {
if (oaParams == null)
return new ArrayList<>();
return oaParams.stream().filter(oaParam -> in.equals(oaParam.getIn())).collect(Collectors.toList());
}

private boolean isObject(Parameter oaParam) {
return "object".equals(oaParam.getSchema().getType());
}

private boolean isArray(Parameter oaParam) {
return "array".equals(oaParam.getSchema().getType());
}

}
Loading
Loading