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

added feature to generate test cases based on specification #205

Merged
merged 9 commits into from
Jan 28, 2019
5 changes: 4 additions & 1 deletion light-rest-4j/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@
<groupId>com.thoughtworks.qdox</groupId>
<artifactId>qdox</artifactId>
</dependency>

<dependency>
<groupId>com.github.mifmif</groupId>
<artifactId>generex</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import static java.io.File.separator;

Expand Down Expand Up @@ -273,7 +274,6 @@ public void generate(String targetPath, Object model, Any config) throws IOExcep

// handler test cases
transfer(targetPath, ("src.test.java." + handlerPackage + ".").replace(".", separator), "TestServer.java", templates.rest.testServer.template(handlerPackage));

for(Map<String, Object> op : operationList){
if(checkExist(targetPath, ("src.test.java." + handlerPackage).replace(".", separator), op.get("handlerName") + "Test.java") && !overwriteHandlerTest) {
continue;
Expand Down Expand Up @@ -338,7 +338,6 @@ private void initializePropertyMap(Entry<String, Any> entry, Map<String, Any> pr
* Handle elements listed as "properties"
*
* @param props The properties map to add to
* @param entrySchema The schema where the properties are listed
*/
//private void handleProperties(List<Map<String, Any>> props, Map.Entry<String, Any> entrySchema) {
private void handleProperties(List<Map<String, Any>> props, Map<String, Any> properties) {
Expand Down Expand Up @@ -481,9 +480,16 @@ public List<Map<String, Object>> getOperationList(Object model) {
flattened.put("capMethod", entryOps.getKey().substring(0, 1).toUpperCase() + entryOps.getKey().substring(1));
flattened.put("path", basePath + path);
String normalizedPath = path.replace("{", "").replace("}", "");
flattened.put("normalizedPath", basePath + normalizedPath);
flattened.put("handlerName", Utils.camelize(normalizedPath) + Utils.camelize(entryOps.getKey()) + "Handler");
Operation operation = entryOps.getValue();
flattened.put("normalizedPath", UrlGenerator.generateUrl(basePath, path, entryOps.getValue().getParameters()));
//eg. 200 || statusCode == 400 || statusCode == 500
flattened.put("supportedStatusCodesStr", operation.getResponses().keySet().stream().collect(Collectors.joining(" || statusCode = ")));
Map<String, Object> headerNameValueMap = operation.getParameters()
.stream()
.filter(parameter -> parameter.getIn().equals("header"))
.collect(Collectors.toMap(k -> k.getName(), v -> UrlGenerator.generateValidParam(v)));
flattened.put("headerNameValueMap", headerNameValueMap);
if (enableParamDescription) {
//get parameters info and put into result
List<Parameter> parameterRawList = operation.getParameters();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.networknt.codegen.rest;

import com.mifmif.common.regex.Generex;
import com.networknt.oas.model.Parameter;
import com.networknt.oas.model.Schema;
import com.networknt.utility.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

/**
* This class is to generate random url based on com.networknt.oas.model.Schema;
*/
public class UrlGenerator {
private static final String NUMBER = "number";
private static final String INTEGER = "integer";
private static final String STRING = "string";
private static final String BOOLEAN = "boolean";
private static final String INT32 = "int32";
private static final String INT64 = "int64";
private static final String FLOAT = "float";
private static final String DOUBLE = "double";
private static final String TRUE = "true";
private static final String FALSE = "false";
private static final int DEFAULT_MIN_NUM = 1;
private static final int DEFAULT_MAX_NUM = 100;
private static final int DEFAULT_MIN_LENGTH = 5;
private static final int DEFAULT_MAX_LENGTH = 30;
//replace ${}
private static final String PATH_TEMPLATE_PATTERN = "\\{(.*?)\\}";
private static final String DEFAULT_STR_PATTERN = "[a-zA-Z]+";
private static final String IN_PATH = "path";
private static final String IN_QUERY = "query";
private static final Logger logger = LoggerFactory.getLogger(UrlGenerator.class);

/**
*
* @param basePath base path of the url
* @param path path of the url
* @param parameters all the parameters under an operation
* @return String generated url
*/
public static String generateUrl(String basePath, String path, List<Parameter> parameters) {
String url = basePath + path;
if(!parameters.isEmpty()){
Optional<Parameter> pathParameter = parameters.stream()
.filter(parameter -> IN_PATH.equals(parameter.getIn()))
.findFirst();
if(pathParameter.isPresent()) {
//generate a valid path parameter then replace {} with it.
String pathParameterStr = generateValidParam(pathParameter.get());
path = path.replaceAll(PATH_TEMPLATE_PATTERN, pathParameterStr);
}

url = basePath + path + generateQueryParamUrl(parameters);
}
return url;
}

/**
* based on parameter schemas generate query parameter part of the url
*/
public static String generateQueryParamUrl(List<Parameter> parameters) {
String url = "";
url += parameters.stream()
.filter(parameter -> IN_QUERY.equals(parameter.getIn()))
.map(parameter -> parameter.getName() + "=" + generateValidParam(parameter))
.collect(Collectors.joining("&"));
//if query params have value, put a "?" in ahead
url = StringUtils.isBlank(url) ? "" : "?" + url;
return url;
}

private static String generateEncodedValidParam(Parameter parameter) {
String encoded = "";
try {
encoded = URLEncoder.encode(generateValidParam(parameter), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encoded;
}

/**
* generate a valid parameter value based on schema of that parameter
* @param parameter parameter under an operation
* @return String parameter value
*/
protected static String generateValidParam(Parameter parameter) {
String validParam = "";
Schema schema = parameter.getSchema();
if(!schema.getAllOfSchemas().isEmpty() || !schema.getOneOfSchemas().isEmpty()){
logger.info("dont support one of/ all of schema test case generation");
return "";
} else if(!schema.getAnyOfSchemas().isEmpty()) {
schema = schema.getAnyOfSchemas().get(0);
}
String type = schema.getType();
if (!StringUtils.isEmpty(type)) {
if(NUMBER.equals(type.toLowerCase()) || INTEGER.equals(type.toLowerCase())) {
validParam = generateValidNum(schema);
} else if(type.toLowerCase().equals(STRING)) {
validParam = generateValidStr(schema);
} else if(type.toLowerCase().equals(BOOLEAN)) {
validParam = generateValidBool(schema);
} else {
logger.info("unsupported param type to generate test case: {}/ {}", parameter.getName(), type);
}
}
return validParam;
}

/**
* generate bool based on schema
* @param schema schema of a parameter
* @return "true" or "false"
*/
private static String generateValidBool(Schema schema) {
return ThreadLocalRandom.current().nextBoolean() ? TRUE : FALSE;
}

/**
* generate String based on schema: minLength, maxLength, pattern
* @param schema schema of a parameter
* @return String
*/
private static String generateValidStr(Schema schema) {
String pattern = schema.getPattern() == null ? DEFAULT_STR_PATTERN : schema.getPattern();
int minLength = schema.getMinLength() == null ? DEFAULT_MIN_LENGTH : schema.getMinLength();
int maxLength = schema.getMaxLength() == null ? DEFAULT_MAX_LENGTH : schema.getMaxLength();
Generex generex = new Generex(pattern);
return generex.random(minLength, maxLength);
}

/**
* generate number based on schema: format, minimum, maximum, exclusiveMinimum, exclusiveMaximum,
* @param schema
* @return String generated number
*/
private static String generateValidNum(Schema schema) {
//if format is empty, consider it's an int.
String format = StringUtils.isBlank(schema.getFormat()) ? INT32 : schema.getFormat();
Number min = schema.getMinimum() == null ? Integer.valueOf(DEFAULT_MIN_NUM) : schema.getMinimum();
Number max = schema.getMaximum() == null ? Integer.valueOf(DEFAULT_MAX_NUM) : schema.getMaximum();
String validNumStr = "";
if(INT32.equals(format) || INT64.equals(format)) {
int validInt;
do {
//validInt is from [min, max]
validInt = ThreadLocalRandom.current().nextInt(min.intValue(), max.intValue());
//when exclusiveMinimum || exclusiveMaximum is true, validDouble shouldn't equals to minimum/max value, regenerate.
} while ((Boolean.TRUE.equals(schema.getExclusiveMinimum()) && validInt == min.intValue())
|| (Boolean.TRUE.equals(schema.getExclusiveMaximum()) && validInt == max.intValue()));
validNumStr = String.valueOf(validInt);
} else if(DOUBLE.equals(format) || FLOAT.equals(format)) {
double validDouble;
do {
//validDouble is from [min, max]
validDouble = ThreadLocalRandom.current().nextDouble(min.doubleValue(), max.doubleValue());
//when exclusiveMinimum || exclusiveMaximum is true, validDouble shouldn't equals to minimum/max value, regenerate.
} while ((Boolean.TRUE.equals(schema.getExclusiveMinimum()) && validDouble == min.intValue())
|| (Boolean.TRUE.equals(schema.getExclusiveMaximum()) && validDouble == max.intValue()));
validNumStr = String.valueOf(validDouble);
}
return validNumStr;
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
@import java.util.Map
@import com.jsoniter.any.Any
@import java.util.stream.Collectors
@args (String handlerPackage, Map<String, Object> map)
package @handlerPackage;

import com.networknt.client.Http2Client;
import com.networknt.exception.ApiException;
import com.networknt.exception.ClientException;
import com.networknt.openapi.ResponseValidator;
import com.networknt.schema.SchemaValidatorsConfig;
import com.networknt.status.Status;
import com.networknt.utility.StringUtils;
import io.undertow.UndertowOptions;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientRequest;
import io.undertow.client.ClientResponse;
import io.undertow.util.HeaderValues;
import io.undertow.util.HttpString;
import io.undertow.util.Headers;
import io.undertow.util.Methods;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.Ignore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnio.IoUtils;
import org.xnio.OptionMap;
import java.net.URI;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

import java.io.IOException;

@with (className = map.get("handlerName") + "Test",
method = map.get("handlerName") + "Test()",
loggerName = map.get("handlerName") + "Test" + ".class",
httpMethod = map.get("method"),
hasBody = ("POST".equals(map.get("method").toString()) || "PUT".equals(map.get("method").toString()) || "PATCH".equals(map.get("method").toString())),
path = map.get("normalizedPath")) {
path = map.get("normalizedPath"),
supportedStatusCodesStr = map.get("supportedStatusCodesStr"),
headerNameValueMap = (Map)map.get("headerNameValueMap")) {
@@Ignore
public class @className {
@@ClassRule
public static TestServer server = TestServer.getInstance();
Expand All @@ -40,10 +50,11 @@ public class @className {
static final int httpPort = server.getServerConfig().getHttpPort();
static final int httpsPort = server.getServerConfig().getHttpsPort();
static final String url = enableHttp2 || enableHttps ? "https://localhost:" + httpsPort : "http://localhost:" + httpPort;
static final String JSON_MEDIA_TYPE = "application/json";

@@Test
public void test@method throws ClientException, ApiException {
/*
public void test@method throws ClientException {

final Http2Client client = Http2Client.getInstance();
final CountDownLatch latch = new CountDownLatch(1);
final ClientConnection connection;
Expand All @@ -53,13 +64,19 @@ public class @className {
throw new ClientException(e);
}
final AtomicReference<ClientResponse> reference = new AtomicReference<>();
String requestUri = "@path";
String httpMethod = "@httpMethod.toString().toLowerCase()";
try {
ClientRequest request = new ClientRequest().setPath("@path").setMethod(Methods.@httpMethod);
ClientRequest request = new ClientRequest().setPath(requestUri).setMethod(Methods.@httpMethod);
@if(hasBody) {
request.getRequestHeaders().put(Headers.CONTENT_TYPE, "application/json");
request.getRequestHeaders().put(Headers.CONTENT_TYPE, JSON_MEDIA_TYPE);
request.getRequestHeaders().put(Headers.TRANSFER_ENCODING, "chunked");
connection.sendRequest(request, client.createClientCallback(reference, latch, "request body to be replaced"));
//customized header parameters @for((String key, Object item): headerNameValueMap) {
request.getRequestHeaders().put(new HttpString("@key"), "@item");}
connection.sendRequest(request, client.createClientCallback(reference, latch, "{\"content\": \"request body to be replaced\"}"));
} else {
//customized header parameters @for((String key, Object item): headerNameValueMap) {
request.getRequestHeaders().put(new HttpString("@key"), "@item");}
connection.sendRequest(request, client.createClientCallback(reference, latch));
}
latch.await();
Expand All @@ -69,11 +86,19 @@ public class @className {
} finally {
IoUtils.safeClose(connection);
}
int statusCode = reference.get().getResponseCode();
String body = reference.get().getAttachment(Http2Client.RESPONSE_BODY);
Assert.assertEquals(200, statusCode);
Assert.assertNotNull(body);
*/
Optional<HeaderValues> contentTypeName = Optional.ofNullable(reference.get().getResponseHeaders().get(Headers.CONTENT_TYPE));
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setMissingNodeAsError(true);
ResponseValidator responseValidator = new ResponseValidator(config);
int statusCode = reference.get().getResponseCode();
Status status;
if(contentTypeName.isPresent()) {
status = responseValidator.validateResponseContent(body, requestUri, httpMethod, String.valueOf(statusCode), contentTypeName.get().getFirst());
} else {
status = responseValidator.validateResponseContent(body, requestUri, httpMethod, String.valueOf(statusCode), JSON_MEDIA_TYPE);
}
Assert.assertNull(status);
}
}
}
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
<version.fastscanner>2.18.1</version.fastscanner>
<version.graphql>8.0</version.graphql>
<version.qdox>2.0-M9</version.qdox>
<version.generex>1.0.2</version.generex>
<versions.maven-version>2.4</versions.maven-version>
<argLine>-Xmx512m -XX:MaxPermSize=256m</argLine>
</properties>
Expand Down Expand Up @@ -294,6 +295,11 @@
<artifactId>qdox</artifactId>
<version>${version.qdox}</version>
</dependency>
<dependency>
<groupId>com.github.mifmif</groupId>
<artifactId>generex</artifactId>
<version>${version.generex}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down