[core] Extracting recommendations to validation framework (#4979)

* [core] Extracting recommendations to validation framework

This is work to extract recommendation logic out of the CLI validate command
into a shared evaluation instance which can be used elsewhere (such as Gradle,
or the Online tool).

For now, these validations are in addition to those provided by swagger-parser and
are only the following recommendations:

* Apache/Nginx warning that header values with underscore are dropped by default
* Unused models/schemas
* Use of properties with oneOf, which is ambiguous in OpenAPI Specification

I've allowed for disabling recommendations via System properties, since this is
something that has been requested a few times by users. System properties in this
commit include:

* openapi.generator.rule.recommendations=false
  - Allows for disabling recommendations completely. This wouldn't include all warnings
    and errors, only those we deem to be suggestions
* openapi.generator.rule.apache-nginx-underscore=false
  - Allows for disabling the Apache/Nginx warning when header names have underscore
  - This is a legacy CGI configuration, and doesn't affect all web servers
* openapi.generator.rule.oneof-properties-ambiguity=false
  - We support this functionality, but the specification may not intend for it
  - This is more to reduce noise
* openapi.generator.rule.unused-schemas=false
  - We will warn when a schema is not referenced outside of Components, which
    users have requested to be able to turn off
* openapi.generator.rule.anti-patterns.uri-unexpected-body=false

* Move recommendation/validations to oas package and add javadoc comments

* Refactor and test recommendation validations

* Refactor validation function signatures to return explicit state rather than boolean

* Add operation recommendation for GET/HEAD w/body
This commit is contained in:
Jim Schubert 2020-01-31 12:19:16 -05:00 committed by GitHub
parent f06ac9d91c
commit 22c6c0ca68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1089 additions and 75 deletions

View File

@ -22,14 +22,14 @@ import io.airlift.airline.Option;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.openapitools.codegen.utils.ModelUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.openapitools.codegen.validation.ValidationResult;
import org.openapitools.codegen.validations.oas.OpenApiEvaluator;
import org.openapitools.codegen.validations.oas.RuleConfiguration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Command(name = "validate", description = "Validate specification")
@ -54,42 +54,28 @@ public class Validate implements Runnable {
StringBuilder sb = new StringBuilder();
OpenAPI specification = result.getOpenAPI();
if (Boolean.TRUE.equals(recommend)) {
if (specification != null) {
// Add information about unused models to the warnings set.
List<String> unusedModels = ModelUtils.getUnusedSchemas(specification);
if (unusedModels != null) {
unusedModels.forEach(name -> warnings.add("Unused model: " + name));
}
RuleConfiguration ruleConfiguration = new RuleConfiguration();
ruleConfiguration.setEnableRecommendations(recommend != null ? recommend : false);
// check for loosely defined oneOf extension requirements.
// This is a recommendation because the 3.0.x spec is not clear enough on usage of oneOf.
// see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'.
Map<String, Schema> schemas = ModelUtils.getSchemas(specification);
schemas.forEach((key, schema) -> {
if (schema instanceof ComposedSchema) {
final ComposedSchema composed = (ComposedSchema) schema;
if (composed.getOneOf() != null && composed.getOneOf().size() > 0) {
if (composed.getProperties() != null && composed.getProperties().size() >= 1 && composed.getProperties().get("discriminator") == null) {
warnings.add("Schema (oneOf) should not contain properties: " + key);
}
}
}
});
}
}
OpenApiEvaluator evaluator = new OpenApiEvaluator(ruleConfiguration);
ValidationResult validationResult = evaluator.validate(specification);
// TODO: We could also provide description here along with getMessage. getMessage is either a "generic" message or specific (e.g. Model 'Cat' has issues).
// This would require that we parse the messageList coming from swagger-parser into a better structure.
validationResult.getWarnings().forEach(invalid -> warnings.add(invalid.getMessage()));
validationResult.getErrors().forEach(invalid -> errors.add(invalid.getMessage()));
if (!errors.isEmpty()) {
sb.append("Errors:").append(System.lineSeparator());
errors.forEach(msg ->
sb.append("\t-").append(msg).append(System.lineSeparator())
sb.append("\t- ").append(WordUtils.wrap(msg, 90).replace(System.lineSeparator(), System.lineSeparator() + "\t ")).append(System.lineSeparator())
);
}
if (!warnings.isEmpty()) {
sb.append("Warnings: ").append(System.lineSeparator());
warnings.forEach(msg ->
sb.append("\t-").append(msg).append(System.lineSeparator())
sb.append("\t- ").append(WordUtils.wrap(msg, 90).replace(System.lineSeparator(), System.lineSeparator() + "\t ")).append(System.lineSeparator())
);
}

View File

@ -23,9 +23,8 @@ import java.util.List;
*
* @param <TInput> The type of object being evaluated.
*/
@SuppressWarnings({"WeakerAccess"})
public class GenericValidator<TInput> implements Validator<TInput> {
private List<ValidationRule> rules;
protected List<ValidationRule> rules;
/**
* Constructs a new instance of {@link GenericValidator}.
@ -48,11 +47,11 @@ public class GenericValidator<TInput> implements Validator<TInput> {
ValidationResult result = new ValidationResult();
if (rules != null) {
rules.forEach(it -> {
boolean passes = it.evaluate(input);
if (passes) {
ValidationRule.Result attempt = it.evaluate(input);
if (attempt.passed()) {
result.addResult(Validated.valid(it));
} else {
result.addResult(Validated.invalid(it, it.getFailureMessage()));
result.addResult(Validated.invalid(it, it.getFailureMessage(), attempt.getDetails()));
}
});
}

View File

@ -23,6 +23,7 @@ package org.openapitools.codegen.validation;
public final class Invalid extends Validated {
private String message;
private ValidationRule rule;
private String details;
/**
* Constructs a new {@link Invalid} instance.
@ -35,13 +36,29 @@ public final class Invalid extends Validated {
this.message = message;
}
/**
* Constructs a new {@link Invalid} instance.
*
* @param rule The rule which was evaluated and resulted in this state.
* @param message The message to be displayed for this invalid state.
* @param details Additional contextual details related to the invalid state.
*/
public Invalid(ValidationRule rule, String message, String details) {
this(rule, message);
this.details = details;
}
public String getDetails() {
return details;
}
@Override
String getMessage() {
public String getMessage() {
return message;
}
@Override
ValidationRule getRule() {
public ValidationRule getRule() {
return rule;
}

View File

@ -54,6 +54,18 @@ public abstract class Validated {
public static Validated invalid(ValidationRule rule, String message) {
return new Invalid(rule, message);
}
/**
* Creates an instance of an {@link Invalid} validation state.
*
* @param rule The rule which was evaluated.
* @param message The message to display to a user.
* @param details Additional contextual details related to the invalid state.
*
* @return A {@link Validated} instance representing an invalid state according to the rule.
*/
public static Validated invalid(ValidationRule rule, String message, String details) {
return new Invalid(rule, message, details);
}
/**
* Creates an instance of an {@link Valid} validation state.

View File

@ -24,7 +24,6 @@ import java.util.stream.Collectors;
/**
* Encapsulates details about the result of a validation test.
*/
@SuppressWarnings("WeakerAccess")
public final class ValidationResult {
private final List<Validated> validations;
@ -96,9 +95,16 @@ public final class ValidationResult {
public void addResult(Validated validated) {
synchronized (validations) {
ValidationRule rule = validated.getRule();
if (rule != null && !rule.equals(ValidationRule.empty())) {
if (rule != null && !rule.equals(ValidationRule.empty()) && !validations.contains(validated)) {
validations.add(validated);
}
}
}
public ValidationResult consume(ValidationResult other) {
synchronized (validations) {
validations.addAll(other.validations);
}
return this;
}
}

View File

@ -26,7 +26,7 @@ public class ValidationRule {
private Severity severity;
private String description;
private String failureMessage;
private Function<Object, Boolean> test;
private Function<Object, Result> test;
/**
* Constructs a new instance of {@link ValidationRule}
@ -37,7 +37,7 @@ public class ValidationRule {
* @param test The test condition to be applied as a part of this rule, when this function returns <code>true</code>,
* the evaluated instance will be considered "valid" according to this rule.
*/
ValidationRule(Severity severity, String description, String failureMessage, Function<Object, Boolean> test) {
ValidationRule(Severity severity, String description, String failureMessage, Function<Object, Result> test) {
this.severity = severity;
this.description = description;
this.failureMessage = failureMessage;
@ -60,7 +60,7 @@ public class ValidationRule {
*
* @return <code>true</code> if the object state is valid according to this rule, otherwise <code>false</code>.
*/
public boolean evaluate(Object input) {
public Result evaluate(Object input) {
return test.apply(input);
}
@ -90,7 +90,7 @@ public class ValidationRule {
* @return An "empty" rule.
*/
static ValidationRule empty() {
return new ValidationRule(Severity.ERROR, "empty", "failure message", (i) -> false);
return new ValidationRule(Severity.ERROR, "empty", "failure message", (i) -> Fail.empty() );
}
/**
@ -106,8 +106,8 @@ public class ValidationRule {
* @return A new instance of a {@link ValidationRule}
*/
@SuppressWarnings("unchecked")
public static <T> ValidationRule create(Severity severity, String description, String failureMessage, Function<T, Boolean> fn) {
return new ValidationRule(severity, description, failureMessage, (Function<Object, Boolean>) fn);
public static <T> ValidationRule create(Severity severity, String description, String failureMessage, Function<T, Result> fn) {
return new ValidationRule(severity, description, failureMessage, (Function<Object, Result>) fn);
}
/**
@ -121,8 +121,8 @@ public class ValidationRule {
* @return A new instance of a {@link ValidationRule}
*/
@SuppressWarnings("unchecked")
public static <T> ValidationRule error(String failureMessage, Function<T, Boolean> fn) {
return new ValidationRule(Severity.ERROR, null, failureMessage, (Function<Object, Boolean>) fn);
public static <T> ValidationRule error(String failureMessage, Function<T, Result> fn) {
return new ValidationRule(Severity.ERROR, null, failureMessage, (Function<Object, Result>) fn);
}
/**
@ -137,8 +137,8 @@ public class ValidationRule {
* @return A new instance of a {@link ValidationRule}
*/
@SuppressWarnings("unchecked")
public static <T> ValidationRule warn(String description, String failureMessage, Function<T, Boolean> fn) {
return new ValidationRule(Severity.WARNING, description, failureMessage, (Function<Object, Boolean>) fn);
public static <T> ValidationRule warn(String description, String failureMessage, Function<T, Result> fn) {
return new ValidationRule(Severity.WARNING, description, failureMessage, (Function<Object, Result>) fn);
}
@Override
@ -149,4 +149,69 @@ public class ValidationRule {
", failureMessage='" + failureMessage + '\'' +
'}';
}
public static abstract class Result {
protected String details = null;
protected Throwable throwable = null;
public String getDetails() {
return details;
}
public void setDetails(String details) {
assert this.details == null;
this.details = details;
}
public abstract boolean passed();
public final boolean failed() { return !passed(); }
public Throwable getThrowable() {
return throwable;
}
public boolean thrown() { return this.throwable == null; }
}
public static final class Pass extends Result {
public static Result empty() { return new Pass(); }
public Pass() {
super();
}
public Pass(String details) {
this();
this.details = details;
}
@Override
public boolean passed() {
return true;
}
}
public static final class Fail extends Result {
public static Result empty() { return new Fail(); }
public Fail() {
super();
}
public Fail(String details) {
this();
this.details = details;
}
public Fail(String details, Throwable throwable) {
this();
this.throwable = throwable;
this.details = details;
}
@Override
public boolean passed() {
return false;
}
}
}

View File

@ -25,7 +25,7 @@ import java.util.List;
import java.util.Optional;
public class GenericValidatorTest {
class Person {
static class Person {
private int age;
private String name;
@ -35,33 +35,33 @@ public class GenericValidatorTest {
}
}
private static boolean isValidAge(Person person) {
return person.age > 0;
private static ValidationRule.Result checkAge(Person person) {
return person.age > 0 ? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
private static boolean isAdult(Person person) {
return person.age > 18;
private static ValidationRule.Result checkAdult(Person person) {
return person.age > 18 ? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
private static boolean isNameSet(Person person) {
return person.name != null && person.name.length() > 0;
private static ValidationRule.Result checkName(Person person) {
return (person.name != null && person.name.length() > 0) ? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
private static boolean isNameValid(Person person) {
private static ValidationRule.Result checkNamePattern(Person person) {
String pattern = "^[A-Z][a-z]*$";
return person.name.matches(pattern);
return person.name.matches(pattern) ? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
private static boolean isNameNormalLength(Person person) {
return person.name.length() < 10;
private static ValidationRule.Result checkNameNormalLength(Person person) {
return person.name.length() < 10? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
private List<ValidationRule> validationRules = Arrays.asList(
ValidationRule.error("Age must be positive and more than zero", GenericValidatorTest::isValidAge),
ValidationRule.error("Only adults (18 years old and older)", GenericValidatorTest::isAdult),
ValidationRule.error("Name isn't set!", GenericValidatorTest::isNameSet),
ValidationRule.error("Name isn't formatted correct", GenericValidatorTest::isNameValid),
ValidationRule.warn("Name too long?", "Name may be too long.", GenericValidatorTest::isNameNormalLength)
ValidationRule.error("Age must be positive and more than zero", GenericValidatorTest::checkAge),
ValidationRule.error("Only adults (18 years old and older)", GenericValidatorTest::checkAdult),
ValidationRule.error("Name isn't set!", GenericValidatorTest::checkName),
ValidationRule.error("Name isn't formatted correct", GenericValidatorTest::checkNamePattern),
ValidationRule.warn("Name too long?", "Name may be too long.", GenericValidatorTest::checkNameNormalLength)
);
@Test

View File

@ -33,13 +33,13 @@ public class ValidationRuleTest {
}
}
private static boolean checkName(Sample input) {
return input.getName() != null && input.getName().length() > 7;
private static ValidationRule.Result checkName(Sample input) {
return (input.getName() != null && input.getName().length() > 7) ? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
private static boolean checkPattern(Sample input) {
private static ValidationRule.Result checkPattern(Sample input) {
String pattern = "^[A-Z][a-z]*$";
return input.getName() != null && input.getName().matches(pattern);
return (input.getName() != null && input.getName().matches(pattern)) ? ValidationRule.Pass.empty() : ValidationRule.Fail.empty();
}
@Test
@ -49,10 +49,10 @@ public class ValidationRuleTest {
Sample seven = new Sample("1234567");
Sample eight = new Sample("12345678");
ValidationRule result = ValidationRule.error("test", ValidationRuleTest::checkName);
assertFalse(result.evaluate(nil));
assertFalse(result.evaluate(six));
assertFalse(result.evaluate(seven));
assertTrue(result.evaluate(eight));
assertFalse(result.evaluate(nil).passed());
assertFalse(result.evaluate(six).passed());
assertFalse(result.evaluate(seven).passed());
assertTrue(result.evaluate(eight).passed());
}
@Test
@ -61,8 +61,8 @@ public class ValidationRuleTest {
Sample lowercase = new Sample("jim");
Sample titlecase = new Sample("Jim");
ValidationRule result = ValidationRule.error("test", i -> checkPattern((Sample)i));
assertFalse(result.evaluate(nil));
assertFalse(result.evaluate(lowercase));
assertTrue(result.evaluate(titlecase));
assertFalse(result.evaluate(nil).passed());
assertFalse(result.evaluate(lowercase).passed());
assertTrue(result.evaluate(titlecase).passed());
}
}

View File

@ -0,0 +1,94 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.validation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A validator which evaluates an OpenAPI 3.x specification document
*/
public class OpenApiEvaluator implements Validator<OpenAPI> {
private RuleConfiguration ruleConfiguration;
/**
* Constructs a new instance of {@link OpenApiEvaluator} with applied rules.
*
* @param ruleConfiguration The set of rules to be applied to evaluation.
*/
public OpenApiEvaluator(RuleConfiguration ruleConfiguration) {
this.ruleConfiguration = ruleConfiguration;
}
/**
* Validates input, resulting in a instance of {@link ValidationResult} which provides details on all validations performed (success, error, warning).
*
* @param specification The {@link OpenAPI} object instance to be validated.
* @return A {@link ValidationResult} which details the success, error, and warning validation results.
*/
@Override
public ValidationResult validate(OpenAPI specification) {
ValidationResult validationResult = new ValidationResult();
if (specification == null) return validationResult;
OpenApiParameterValidations parameterValidations = new OpenApiParameterValidations(ruleConfiguration);
OpenApiSecuritySchemeValidations securitySchemeValidations = new OpenApiSecuritySchemeValidations(ruleConfiguration);
OpenApiSchemaValidations schemaValidations = new OpenApiSchemaValidations(ruleConfiguration);
OpenApiOperationValidations operationValidations = new OpenApiOperationValidations(ruleConfiguration);
if (ruleConfiguration.isEnableUnusedSchemasRecommendation()) {
ValidationRule unusedSchema = ValidationRule.create(Severity.WARNING, "Unused schema", "A schema was determined to be unused.", s -> ValidationRule.Pass.empty());
ModelUtils.getUnusedSchemas(specification).forEach(schemaName -> validationResult.addResult(Validated.invalid(unusedSchema, "Unused model: " + schemaName)));
}
Map<String, Schema> schemas = ModelUtils.getSchemas(specification);
schemas.forEach((key, schema) -> validationResult.consume(schemaValidations.validate(schema)));
List<Parameter> parameters = new ArrayList<>(50);
Paths paths = specification.getPaths();
if (paths != null) {
paths.forEach((key, pathItem) -> {
// parameters defined "globally"
List<Parameter> pathParameters = pathItem.getParameters();
if (pathParameters != null) parameters.addAll(pathItem.getParameters());
pathItem.readOperationsMap().forEach((httpMethod, op) -> {
if (op != null) {
// parameters on each operation method
if (op.getParameters() != null) {
parameters.addAll(op.getParameters());
}
OperationWrapper wrapper = new OperationWrapper(op, httpMethod);
validationResult.consume(operationValidations.validate(wrapper));
}
});
});
}
Components components = specification.getComponents();
if (components != null) {
Map<String, SecurityScheme> securitySchemes = components.getSecuritySchemes();
if (securitySchemes != null && !securitySchemes.isEmpty()) {
securitySchemes.values().forEach(securityScheme -> validationResult.consume(securitySchemeValidations.validate(securityScheme)));
}
if (components.getParameters() != null) {
parameters.addAll(components.getParameters().values());
}
}
parameters.forEach(parameter -> validationResult.consume(parameterValidations.validate(parameter)));
return validationResult;
}
}

View File

@ -0,0 +1,75 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.RequestBody;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.validation.GenericValidator;
import org.openapitools.codegen.validation.ValidationRule;
import java.util.ArrayList;
import java.util.Locale;
/**
* A standalone instance for evaluating rule and recommendations related to OAS {@link io.swagger.v3.oas.models.Operation}
*/
class OpenApiOperationValidations extends GenericValidator<OperationWrapper> {
OpenApiOperationValidations(RuleConfiguration ruleConfiguration) {
super(new ArrayList<>());
if (ruleConfiguration.isEnableRecommendations()) {
if (ruleConfiguration.isEnableApiRequestUriWithBodyRecommendation()) {
rules.add(ValidationRule.warn(
"API GET/HEAD defined with request body",
"While technically allowed, GET/HEAD with request body may indicate programming error, and is considered an anti-pattern.",
OpenApiOperationValidations::checkAntipatternGetOrHeadWithBody
));
}
}
}
/**
* Determines whether a GET or HEAD operation is configured to expect a body.
* <p>
* RFC7231 describes this behavior as:
* <p>
* A payload within a GET request message has no defined semantics;
* sending a payload body on a GET request might cause some existing
* implementations to reject the request.
* <p>
* See https://tools.ietf.org/html/rfc7231#section-4.3.1
* <p>
* Because there are no defined semantics, and because some client and server implementations
* may silently ignore the entire body (see https://xhr.spec.whatwg.org/#the-send()-method) or
* throw an error (see https://fetch.spec.whatwg.org/#ref-for-dfn-throw%E2%91%A1%E2%91%A1),
* we maintain that the existence of a body for this operation is most likely programmer error and raise awareness.
*
* @param wrapper Wraps an operation with accompanying HTTP Method
* @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail}
*/
private static ValidationRule.Result checkAntipatternGetOrHeadWithBody(OperationWrapper wrapper) {
if (wrapper == null) {
return ValidationRule.Pass.empty();
}
ValidationRule.Result result = ValidationRule.Pass.empty();
if (wrapper.getHttpMethod() == PathItem.HttpMethod.GET || wrapper.getHttpMethod() == PathItem.HttpMethod.HEAD) {
RequestBody body = wrapper.getOperation().getRequestBody();
if (body != null) {
if (StringUtils.isNotEmpty(body.get$ref()) || (body.getContent() != null && body.getContent().size() > 0)) {
result = new ValidationRule.Fail();
result.setDetails(String.format(
Locale.ROOT,
"%s %s contains a request body and is considered an anti-pattern.",
wrapper.getHttpMethod().name(),
wrapper.getOperation().getOperationId())
);
}
}
}
return result;
}
}

View File

@ -0,0 +1,47 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.parameters.HeaderParameter;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.validation.GenericValidator;
import org.openapitools.codegen.validation.ValidationRule;
import java.util.ArrayList;
import java.util.Locale;
/**
* A standalone instance for evaluating rules and recommendations related to OAS {@link Parameter}
*/
class OpenApiParameterValidations extends GenericValidator<Parameter> {
OpenApiParameterValidations(RuleConfiguration ruleConfiguration) {
super(new ArrayList<>());
if (ruleConfiguration.isEnableRecommendations()) {
if (ruleConfiguration.isEnableApacheNginxUnderscoreRecommendation()) {
rules.add(ValidationRule.warn(
ValidationConstants.ApacheNginxUnderscoreDescription,
ValidationConstants.ApacheNginxUnderscoreFailureMessage,
OpenApiParameterValidations::apacheNginxHeaderCheck
));
}
}
}
/**
* Apache and Nginx default to legacy CGI behavior in which header with underscore are ignored. Raise this for awareness to the user.
*
* @param parameter Any spec doc parameter. The method will handle {@link HeaderParameter} evaluation.
* @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail} with details "[key] contains an underscore."
*/
private static ValidationRule.Result apacheNginxHeaderCheck(Parameter parameter) {
if (parameter == null || !parameter.getIn().equals("header")) return ValidationRule.Pass.empty();
ValidationRule.Result result = ValidationRule.Pass.empty();
String headerName = parameter.getName();
if (StringUtils.isNotEmpty(headerName) && StringUtils.contains(headerName, '_')) {
result = new ValidationRule.Fail();
result.setDetails(String.format(Locale.ROOT, "%s contains an underscore.", headerName));
}
return result;
}
}

View File

@ -0,0 +1,57 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.validation.GenericValidator;
import org.openapitools.codegen.validation.ValidationRule;
import java.util.ArrayList;
/**
* A standalone instance for evaluating rules and recommendations related to OAS {@link Schema}
*/
class OpenApiSchemaValidations extends GenericValidator<Schema> {
OpenApiSchemaValidations(RuleConfiguration ruleConfiguration) {
super(new ArrayList<>());
if (ruleConfiguration.isEnableRecommendations()) {
if (ruleConfiguration.isEnableOneOfWithPropertiesRecommendation()) {
rules.add(ValidationRule.warn(
"Schema defines properties alongside oneOf.",
"Schemas defining properties and oneOf are not clearly defined in the OpenAPI Specification. While our tooling supports this, it may cause issues with other tools.",
OpenApiSchemaValidations::checkOneOfWithProperties
));
}
}
}
/**
* JSON Schema defines oneOf as a validation property which can be applied to any schema.
* <p>
* OpenAPI Specification is a variant of JSON Schema for which oneOf is defined as:
* "Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema."
* <p>
* Where the only examples of oneOf in OpenAPI Specification are used to define either/or type structures rather than validations.
* Because of this ambiguity in the spec about what is non-standard about oneOf support, we'll warn as a recommendation that
* properties on the schema defining oneOf relationships may not be intentional in the OpenAPI Specification.
*
* @param schema An input schema, regardless of the type of schema
* @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail}
*/
private static ValidationRule.Result checkOneOfWithProperties(Schema schema) {
ValidationRule.Result result = ValidationRule.Pass.empty();
if (schema instanceof ComposedSchema) {
final ComposedSchema composed = (ComposedSchema) schema;
// check for loosely defined oneOf extension requirements.
// This is a recommendation because the 3.0.x spec is not clear enough on usage of oneOf.
// see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'.
if (composed.getOneOf() != null && composed.getOneOf().size() > 0) {
if (composed.getProperties() != null && composed.getProperties().size() >= 1 && composed.getProperties().get("discriminator") == null) {
// not necessarily "invalid" here, but we trigger the recommendation which requires the method to return false.
result = ValidationRule.Fail.empty();
}
}
}
return result;
}
}

View File

@ -0,0 +1,47 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.validation.GenericValidator;
import org.openapitools.codegen.validation.ValidationRule;
import java.util.ArrayList;
import java.util.Locale;
/**
* A standalone instance for evaluating rules and recommendations related to OAS {@link SecurityScheme}
*/
class OpenApiSecuritySchemeValidations extends GenericValidator<SecurityScheme> {
OpenApiSecuritySchemeValidations(RuleConfiguration ruleConfiguration) {
super(new ArrayList<>());
if (ruleConfiguration.isEnableRecommendations()) {
if (ruleConfiguration.isEnableApacheNginxUnderscoreRecommendation()) {
rules.add(ValidationRule.warn(
ValidationConstants.ApacheNginxUnderscoreDescription,
ValidationConstants.ApacheNginxUnderscoreFailureMessage,
OpenApiSecuritySchemeValidations::apacheNginxHeaderCheck
));
}
}
}
/**
* Apache and Nginx default to legacy CGI behavior in which header with underscore are ignored. Raise this for awareness to the user.
*
* @param securityScheme Security schemes are often used as header parameters (e.g. APIKEY).
* @return <code>true</code> if the check succeeds (header does not have an underscore, e.g. 'api_key')
*/
private static ValidationRule.Result apacheNginxHeaderCheck(SecurityScheme securityScheme) {
if (securityScheme == null || securityScheme.getIn() != SecurityScheme.In.HEADER)
return ValidationRule.Pass.empty();
ValidationRule.Result result = ValidationRule.Pass.empty();
String key = securityScheme.getName();
if (StringUtils.contains(key, '_')) {
result = new ValidationRule.Fail();
result.setDetails(String.format(Locale.ROOT, "%s contains an underscore.", key));
}
return result;
}
}

View File

@ -0,0 +1,42 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
/**
* Encapsulates an operation with its HTTP Method. In OAS, the {@link PathItem} structure contains more than what we'd
* want to evaluate for operation-only checks.
*/
public class OperationWrapper {
private Operation operation;
private PathItem.HttpMethod httpMethod;
/**
* Constructs a new instance of {@link OperationWrapper}
*
* @param operation The operation instances to wrap
* @param httpMethod The http method to wrap
*/
OperationWrapper(Operation operation, PathItem.HttpMethod httpMethod) {
this.operation = operation;
this.httpMethod = httpMethod;
}
/**
* Gets the operation associated with the http method
*
* @return An operation instance
*/
public Operation getOperation() {
return operation;
}
/**
* Gets the http method associated with the operation
*
* @return The http method
*/
public PathItem.HttpMethod getHttpMethod() {
return httpMethod;
}
}

View File

@ -0,0 +1,137 @@
package org.openapitools.codegen.validations.oas;
/**
* Allows for configuration of validation rules which will be applied to a specification.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class RuleConfiguration {
private static String propertyPrefix = "openapi.generator.rule";
private boolean enableRecommendations = defaultedBoolean(propertyPrefix + ".recommendations", true);
private boolean enableApacheNginxUnderscoreRecommendation = defaultedBoolean(propertyPrefix + ".apache-nginx-underscore", true);
private boolean enableOneOfWithPropertiesRecommendation = defaultedBoolean(propertyPrefix + ".oneof-properties-ambiguity", true);
private boolean enableUnusedSchemasRecommendation = defaultedBoolean(propertyPrefix + ".unused-schemas", true);
private boolean enableApiRequestUriWithBodyRecommendation = defaultedBoolean(propertyPrefix + ".anti-patterns.uri-unexpected-body", true);
@SuppressWarnings("SameParameterValue")
private static boolean defaultedBoolean(String key, boolean defaultValue) {
String property = System.getProperty(key);
if (property == null) return defaultValue;
return Boolean.parseBoolean(property);
}
/**
* Gets whether we will raise awareness that header parameters with underscore may be ignored in Apache or Nginx by default.
* For more details, see https://stackoverflow.com/a/22856867/151445.
*
* @return <code>true</code> if enabled, <code>false</code> if disabled
*/
public boolean isEnableApacheNginxUnderscoreRecommendation() {
return enableApacheNginxUnderscoreRecommendation;
}
/**
* Enable or Disable the recommendation check for Apache/Nginx potentially ignoring header with underscore by default.
* <p>
* For more details, see {@link RuleConfiguration#isEnableApacheNginxUnderscoreRecommendation()}
*
* @param enableApacheNginxUnderscoreRecommendation <code>true</code> to enable, <code>false</code> to disable
*/
public void setEnableApacheNginxUnderscoreRecommendation(boolean enableApacheNginxUnderscoreRecommendation) {
this.enableApacheNginxUnderscoreRecommendation = enableApacheNginxUnderscoreRecommendation;
}
/**
* Gets whether we will raise awareness a GET or HEAD operation is defined with body.
*
* @return <code>true</code> if enabled, <code>false</code> if disabled
*/
public boolean isEnableApiRequestUriWithBodyRecommendation() {
return enableApiRequestUriWithBodyRecommendation;
}
/**
* Enable or Disable the recommendation check for GET or HEAD operations with bodies.
* <p>
* For more details, see {@link RuleConfiguration#isEnableApiRequestUriWithBodyRecommendation()}
*
* @param enableApiRequestUriWithBodyRecommendation <code>true</code> to enable, <code>false</code> to disable
*/
public void setEnableApiRequestUriWithBodyRecommendation(boolean enableApiRequestUriWithBodyRecommendation) {
this.enableApiRequestUriWithBodyRecommendation = enableApiRequestUriWithBodyRecommendation;
}
/**
* Gets whether the recommendation check for oneOf with sibling properties exists.
* <p>
* JSON Schema defines oneOf as a validation property which can be applied to any schema.
* <p>
* OpenAPI Specification is a variant of JSON Schema for which oneOf is defined as:
* "Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema."
* <p>
* Where the only examples of oneOf in OpenAPI Specification are used to define either/or type structures rather than validations.
* Because of this ambiguity in the spec about what is non-standard about oneOf support, we'll warn as a recommendation that
* properties on the schema defining oneOf relationships may not be intentional in the OpenAPI Specification.
*
* @return <code>true</code> if enabled, <code>false</code> if disabled
*/
public boolean isEnableOneOfWithPropertiesRecommendation() {
return enableOneOfWithPropertiesRecommendation;
}
/**
* Enable or Disable the recommendation check for schemas containing properties and oneOf definitions.
* <p>
* For more details, see {@link RuleConfiguration#isEnableOneOfWithPropertiesRecommendation()}
*
* @param enableOneOfWithPropertiesRecommendation <code>true</code> to enable, <code>false</code> to disable
*/
public void setEnableOneOfWithPropertiesRecommendation(boolean enableOneOfWithPropertiesRecommendation) {
this.enableOneOfWithPropertiesRecommendation = enableOneOfWithPropertiesRecommendation;
}
/**
* Get whether recommendations are enabled.
*
* @return <code>true</code> if enabled, <code>false</code> if disabled
*/
public boolean isEnableRecommendations() {
return enableRecommendations;
}
/**
* Enable or Disable recommendations. Recommendations are either informational or warning level type validations
* which are raised to communicate issues to the user which they may not be aware of, or for which support in the
* tooling/spec may not be clearly defined.
*
* @param enableRecommendations <code>true</code> to enable, <code>false</code> to disable
*/
public void setEnableRecommendations(boolean enableRecommendations) {
this.enableRecommendations = enableRecommendations;
}
/**
* Gets whether the recommendation to check for unused schemas is enabled.
* <p>
* While the tooling may or may not support generation of models representing unused schemas, we take the stance that
* a schema which is defined but not referenced in an operation or by some schema bound to an operation may be a good
* indicator of a programming error. We surface this information to the user in case the orphaned schema(s) are not
* intentional.
*
* @return <code>true</code> if enabled, <code>false</code> if disabled
*/
public boolean isEnableUnusedSchemasRecommendation() {
return enableUnusedSchemasRecommendation;
}
/**
* Enable or Disable the recommendation check for unused schemas.
* <p>
* For more details, see {@link RuleConfiguration#isEnableUnusedSchemasRecommendation()}
*
* @param enableUnusedSchemasRecommendation <code>true</code> to enable, <code>false</code> to disable
*/
public void setEnableUnusedSchemasRecommendation(boolean enableUnusedSchemasRecommendation) {
this.enableUnusedSchemasRecommendation = enableUnusedSchemasRecommendation;
}
}

View File

@ -0,0 +1,6 @@
package org.openapitools.codegen.validations.oas;
final class ValidationConstants {
static String ApacheNginxUnderscoreDescription = "Apache and Nginx may fail on headers keys with underscore!";
static String ApacheNginxUnderscoreFailureMessage = "Apache and Nginx webservers may fail due to legacy CGI constraints enabled by default in which header keys with underscore are disallowed. See https://stackoverflow.com/a/22856867/151445.";
}

View File

@ -0,0 +1,116 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.parameters.RequestBody;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.validation.Invalid;
import org.openapitools.codegen.validation.ValidationResult;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.List;
import java.util.stream.Collectors;
public class OpenApiOperationValidationsTest {
@DataProvider(name = "getOrHeadWithBodyExpectations")
public Object[][] getOrHeadWithBodyExpectations() {
return new Object[][]{
/* method */ /* operationId */ /* ref */ /* content */ /* triggers warning */
{PathItem.HttpMethod.GET, "opWithRef", "#/components/schemas/Animal", null, true},
{PathItem.HttpMethod.GET, "opWithContent", null, new Content().addMediaType("a", new MediaType()), true},
{PathItem.HttpMethod.GET, "opWithoutRefOrContent", null, null, false},
{PathItem.HttpMethod.HEAD, "opWithRef", "#/components/schemas/Animal", null, true},
{PathItem.HttpMethod.HEAD, "opWithContent", null, new Content().addMediaType("a", new MediaType()), true},
{PathItem.HttpMethod.HEAD, "opWithoutRefOrContent", null, null, false},
{PathItem.HttpMethod.POST, "opWithRef", "#/components/schemas/Animal", null, false},
{PathItem.HttpMethod.POST, "opWithContent", null, new Content().addMediaType("a", new MediaType()), false},
{PathItem.HttpMethod.POST, "opWithoutRefOrContent", null, null, false}
};
}
@Test(dataProvider = "getOrHeadWithBodyExpectations")
public void testGetOrHeadWithBody(PathItem.HttpMethod method, String operationId, String ref, Content content, boolean shouldTriggerFailure) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(true);
OpenApiOperationValidations validator = new OpenApiOperationValidations(config);
Operation op = new Operation().operationId(operationId);
RequestBody body = new RequestBody();
if (StringUtils.isNotEmpty(ref) || content != null) {
body.$ref(ref);
body.content(content);
op.setRequestBody(body);
}
ValidationResult result = validator.validate(new OperationWrapper(op, method));
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "API GET/HEAD defined with request body".equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
if (shouldTriggerFailure) {
Assert.assertEquals(warnings.size(), 1, "Expected warnings to include recommendation.");
} else {
Assert.assertEquals(warnings.size(), 0, "Expected warnings not to include recommendation.");
}
}
@Test(dataProvider = "getOrHeadWithBodyExpectations")
public void testGetOrHeadWithBodyWithDisabledRecommendations(PathItem.HttpMethod method, String operationId, String ref, Content content, boolean shouldTriggerFailure) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(false);
OpenApiOperationValidations validator = new OpenApiOperationValidations(config);
Operation op = new Operation().operationId(operationId);
RequestBody body = new RequestBody();
if (StringUtils.isNotEmpty(ref) || content != null) {
body.$ref(ref);
body.content(content);
op.setRequestBody(body);
}
ValidationResult result = validator.validate(new OperationWrapper(op, method));
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "API GET/HEAD defined with request body".equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected warnings not to include recommendation.");
}
@Test(dataProvider = "getOrHeadWithBodyExpectations")
public void testGetOrHeadWithBodyWithDisabledRule(PathItem.HttpMethod method, String operationId, String ref, Content content, boolean shouldTriggerFailure) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableApiRequestUriWithBodyRecommendation(false);
OpenApiOperationValidations validator = new OpenApiOperationValidations(config);
Operation op = new Operation().operationId(operationId);
RequestBody body = new RequestBody();
if (StringUtils.isNotEmpty(ref) || content != null) {
body.$ref(ref);
body.content(content);
op.setRequestBody(body);
}
ValidationResult result = validator.validate(new OperationWrapper(op, method));
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "API GET/HEAD defined with request body".equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected warnings not to include recommendation.");
}
}

View File

@ -0,0 +1,93 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.openapitools.codegen.validation.Invalid;
import org.openapitools.codegen.validation.ValidationResult;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.List;
import java.util.stream.Collectors;
public class OpenApiParameterValidationsTest {
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "disable apache nginx via turning off recommendations")
public void testApacheNginxWithDisabledRecommendations(String in, String key, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(false);
OpenApiParameterValidations validator = new OpenApiParameterValidations(config);
Parameter parameter = new Parameter();
parameter.setIn(in);
parameter.setName(key);
ValidationResult result = validator.validate(parameter);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> ValidationConstants.ApacheNginxUnderscoreFailureMessage.equals(invalid.getMessage()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected recommendations to be disabled completely.");
}
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "disable apache nginx via turning off recommendations")
public void testApacheNginxWithDisabledRule(String in, String key, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableApacheNginxUnderscoreRecommendation(false);
OpenApiParameterValidations validator = new OpenApiParameterValidations(config);
Parameter parameter = new Parameter();
parameter.setIn(in);
parameter.setName(key);
ValidationResult result = validator.validate(parameter);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> ValidationConstants.ApacheNginxUnderscoreFailureMessage.equals(invalid.getMessage()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected rule to be disabled.");
}
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "default apache nginx recommendation")
public void testDefaultRecommendationApacheNginx(String in, String key, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(true);
OpenApiParameterValidations validator = new OpenApiParameterValidations(config);
Parameter parameter = new Parameter();
parameter.setIn(in);
parameter.setName(key);
ValidationResult result = validator.validate(parameter);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> ValidationConstants.ApacheNginxUnderscoreFailureMessage.equals(invalid.getMessage()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
if (matches) {
Assert.assertEquals(warnings.size(), 1, "Expected " + key + " to match recommendation.");
} else {
Assert.assertEquals(warnings.size(), 0, "Expected " + key + " not to match recommendation.");
}
}
@DataProvider(name = "apacheNginxRecommendationExpectations")
public Object[][] apacheNginxRecommendationExpectations() {
return new Object[][]{
{"header", "api_key", true},
{"header", "apikey", false},
{"cookie", "api_key", false},
{"cookie", "apikey", false},
{"query", "api_key", false},
{"query", "apikey", false}
};
}
}

View File

@ -0,0 +1,128 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.media.*;
import org.openapitools.codegen.validation.Invalid;
import org.openapitools.codegen.validation.ValidationResult;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class OpenApiSchemaValidationsTest {
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "default oneOf with sibling properties recommendation")
public void testDefaultRecommendationOneOfWithSiblingProperties(Schema schema, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(true);
OpenApiSchemaValidations validator = new OpenApiSchemaValidations(config);
ValidationResult result = validator.validate(schema);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "Schema defines properties alongside oneOf." .equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
if (matches) {
Assert.assertEquals(warnings.size(), 1, "Expected to match recommendation.");
} else {
Assert.assertEquals(warnings.size(), 0, "Expected not to match recommendation.");
}
}
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "disable oneOf with sibling properties recommendation via turning off recommendations")
public void testOneOfWithSiblingPropertiesDisabledRecommendations(Schema schema, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(false);
OpenApiSchemaValidations validator = new OpenApiSchemaValidations(config);
ValidationResult result = validator.validate(schema);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "Schema defines properties alongside oneOf." .equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected recommendations to be disabled completely.");
}
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "disable oneOf with sibling properties recommendation via turning off rule")
public void testOneOfWithSiblingPropertiesDisabledRule(Schema schema, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableOneOfWithPropertiesRecommendation(false);
OpenApiSchemaValidations validator = new OpenApiSchemaValidations(config);
ValidationResult result = validator.validate(schema);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "Schema defines properties alongside oneOf." .equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected rule to be disabled.");
}
@DataProvider(name = "apacheNginxRecommendationExpectations")
public Object[][] apacheNginxRecommendationExpectations() {
return new Object[][]{
{getOneOfSample(true), true},
{getOneOfSample(false), false},
{getAllOfSample(true), false},
{getAllOfSample(false), false},
{getAnyOfSample(true), false},
{getAnyOfSample(false), false},
{new StringSchema(), false},
{new MapSchema(), false},
{new ArraySchema(), false},
{new ObjectSchema(), false}
};
}
private ComposedSchema getOneOfSample(boolean withProperties) {
ComposedSchema schema = new ComposedSchema().oneOf(Arrays.asList(
new StringSchema(),
new IntegerSchema().format("int64"))
);
if (withProperties) {
schema.addProperties("other", new ArraySchema()
.items(new Schema().$ref("#/definitions/Other")));
}
return schema;
}
private ComposedSchema getAllOfSample(boolean withProperties) {
// This doesn't matter if it's realistic; it's a structural check
ComposedSchema schema = new ComposedSchema().allOf(Arrays.asList(
new StringSchema(),
new IntegerSchema().format("int64"))
);
if (withProperties) {
schema.addProperties("other", new ArraySchema()
.items(new Schema().$ref("#/definitions/Other")));
}
return schema;
}
private ComposedSchema getAnyOfSample(boolean withProperties) {
ComposedSchema schema = new ComposedSchema().anyOf(Arrays.asList(
new StringSchema(),
new IntegerSchema().format("int64"))
);
if (withProperties) {
schema.addProperties("other", new ArraySchema()
.items(new Schema().$ref("#/definitions/Other")));
}
return schema;
}
}

View File

@ -0,0 +1,87 @@
package org.openapitools.codegen.validations.oas;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.openapitools.codegen.validation.Invalid;
import org.openapitools.codegen.validation.ValidationResult;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.List;
import java.util.stream.Collectors;
public class OpenApiSecuritySchemeValidationsTest {
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "disable apache nginx via turning off recommendations")
public void testApacheNginxWithDisabledRecommendations(SecurityScheme.In in, String key, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(false);
OpenApiSecuritySchemeValidations validator = new OpenApiSecuritySchemeValidations(config);
SecurityScheme securityScheme = new SecurityScheme().in(in).name(key);
ValidationResult result = validator.validate(securityScheme);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> ValidationConstants.ApacheNginxUnderscoreFailureMessage.equals(invalid.getMessage()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected recommendations to be disabled completely.");
}
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "disable apache nginx via turning off rule")
public void testApacheNginxWithDisabledRule(SecurityScheme.In in, String key, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableApacheNginxUnderscoreRecommendation(false);
OpenApiSecuritySchemeValidations validator = new OpenApiSecuritySchemeValidations(config);
SecurityScheme securityScheme = new SecurityScheme().in(in).name(key);
ValidationResult result = validator.validate(securityScheme);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> ValidationConstants.ApacheNginxUnderscoreFailureMessage.equals(invalid.getMessage()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
Assert.assertEquals(warnings.size(), 0, "Expected rule to be disabled.");
}
@Test(dataProvider = "apacheNginxRecommendationExpectations", description = "default apache nginx recommendation")
public void testDefaultRecommendationApacheNginx(SecurityScheme.In in, String key, boolean matches) {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(true);
OpenApiSecuritySchemeValidations validator = new OpenApiSecuritySchemeValidations(config);
SecurityScheme securityScheme = new SecurityScheme().in(in).name(key);
ValidationResult result = validator.validate(securityScheme);
Assert.assertNotNull(result.getWarnings());
List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> ValidationConstants.ApacheNginxUnderscoreFailureMessage.equals(invalid.getMessage()))
.collect(Collectors.toList());
Assert.assertNotNull(warnings);
if (matches) {
Assert.assertEquals(warnings.size(), 1, "Expected " + key + " to match recommendation.");
} else {
Assert.assertEquals(warnings.size(), 0, "Expected " + key + " not to match recommendation.");
}
}
@DataProvider(name = "apacheNginxRecommendationExpectations")
public Object[][] apacheNginxRecommendationExpectations() {
return new Object[][]{
{SecurityScheme.In.HEADER, "api_key", true},
{SecurityScheme.In.HEADER, "apikey", false},
{SecurityScheme.In.COOKIE, "api_key", false},
{SecurityScheme.In.COOKIE, "apikey", false},
{SecurityScheme.In.QUERY, "api_key", false},
{SecurityScheme.In.COOKIE, "apikey", false}
};
}
}