diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/OASConfig.java b/api/src/main/java/org/eclipse/microprofile/openapi/OASConfig.java index 5a1504077..5098b0a2f 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/OASConfig.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/OASConfig.java @@ -67,6 +67,11 @@ private OASConfig() { */ public static final String SCAN_EXCLUDE_CLASSES = "mp.openapi.scan.exclude.classes"; + /** + * Configuration property to enable or disable scanning Jakarta Bean Validation annotations + */ + public static final String SCAN_BEANVALIDATION = "mp.openapi.scan.beanvalidation"; + /** * Configuration property to specify the list of global servers that provide connectivity information. * diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java index 3a56ebd10..166f3eebb 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java @@ -69,8 +69,8 @@ /** * A declaration of which security mechanisms can be used across the API. *

- * Adding a {@code SecurityRequirement} to this array is equivalent to adding a {@code SecurityRequirementsSet} containing - * a single {@code SecurityRequirement} to {@link #securitySets()}. + * Adding a {@code SecurityRequirement} to this array is equivalent to adding a {@code SecurityRequirementsSet} + * containing a single {@code SecurityRequirement} to {@link #securitySets()}. * * @return the array of security requirements for this API */ diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirements.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirements.java index c69a59e17..7985f3cbb 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirements.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirements.java @@ -25,8 +25,8 @@ /** * Container annotation for repeated {@link SecurityRequirement} annotations. *

- * Note that each {@code SecurityRequirement} annotation is equivalent to a {@link SecurityRequirementsSet} annotation containing only that - * annotation. + * Note that each {@code SecurityRequirement} annotation is equivalent to a {@link SecurityRequirementsSet} + * annotation containing only that annotation. * *

  * Example: 
diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSet.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSet.java
index 4cf5dab1e..af8d20f00 100644
--- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSet.java
+++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSet.java
@@ -26,18 +26,21 @@
 import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
 
 /**
- * This annotation represents a set of security requirements which permit access to an operation if all of them are satisfied.
+ * This annotation represents a set of security requirements which permit access to an operation if all of them are
+ * satisfied.
  * 

- * If this annotation is applied to a method which corresponds to an operation, then the requirements will be added to that operation. + * If this annotation is applied to a method which corresponds to an operation, then the requirements will be added to + * that operation. *

- * If this annotation is applied to a class which contains methods which correspond to operations, then the requirements will be added to all - * operations corresponding to methods within that class which don't specify any other requirements. + * If this annotation is applied to a class which contains methods which correspond to operations, then the requirements + * will be added to all operations corresponding to methods within that class which don't specify any other + * requirements. *

- * Security requirements can be specified for the whole API using {@link OpenAPIDefinition#securitySets()}. Security requirements specified - * for individual operations override those specified for the whole API. + * Security requirements can be specified for the whole API using {@link OpenAPIDefinition#securitySets()}. Security + * requirements specified for individual operations override those specified for the whole API. *

- * If multiple security requirement sets are specified for an operation, then a user must satisfy all of the requirements within any one of the sets - * to access the operation. + * If multiple security requirement sets are specified for an operation, then a user must satisfy all of the + * requirements within any one of the sets to access the operation. *

* An empty security requirement set indicates that authentication is not required. *

@@ -51,8 +54,8 @@ *

* * @see SecurityRequirement - * Object + * "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#security-requirement-object">SecurityRequirement + * Object **/ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSets.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSets.java index e2ff2b120..2504fe16c 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSets.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/security/SecurityRequirementsSets.java @@ -24,18 +24,23 @@ import java.lang.annotation.Target; /** - * Represents an array of security requirement sets that apply to an operation. Only one of requirement sets needs be satisfied to access the - * operation. + * Represents an array of security requirement sets that apply to an operation. Only one of requirement sets needs be + * satisfied to access the operation. *

- * If this annotation is applied to a method which corresponds to an operation, then the requirements will be added to that operation. + * If this annotation is applied to a method which corresponds to an operation, then the requirements will be added to + * that operation. *

- * If this annotation is applied to a class which contains methods which correspond to operations, then the requirements will be added to all - * operations corresponding to methods within that class which don't specify any other requirements. + * If this annotation is applied to a class which contains methods which correspond to operations, then the requirements + * will be added to all operations corresponding to methods within that class which don't specify any other + * requirements. *

- * This annotation may be used with {@code value} set to an empty array. When applied like this to a method or class, it indicates that no security - * requirements apply to the corresponding operations. This can be used to override security requirements which are specified for the whole API. + * This annotation may be used with {@code value} set to an empty array. When applied like this to a method or class, it + * indicates that no security requirements apply to the corresponding operations. This can be used to override security + * requirements which are specified for the whole API. *

- * A {@code SecurityRequirementSets} annotation corresponds to an array of maps of security requirements in an OpenAPI document. + * A {@code SecurityRequirementSets} annotation corresponds to an array of maps of security requirements in an OpenAPI + * document. + * *

  * Example: 
  * security: 
diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/package-info.java b/api/src/main/java/org/eclipse/microprofile/openapi/package-info.java
index bef2a1a1b..a745ada33 100644
--- a/api/src/main/java/org/eclipse/microprofile/openapi/package-info.java
+++ b/api/src/main/java/org/eclipse/microprofile/openapi/package-info.java
@@ -16,6 +16,6 @@
  * v3 documents from their JAX-RS applications.
  */
 
-@org.osgi.annotation.versioning.Version("2.0")
+@org.osgi.annotation.versioning.Version("2.1")
 @org.osgi.annotation.versioning.ProviderType
 package org.eclipse.microprofile.openapi;
\ No newline at end of file
diff --git a/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc b/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc
index f3e740186..f4577cd13 100644
--- a/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc
+++ b/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc
@@ -119,6 +119,10 @@ Configuration property to specify the list of packages to exclude from scans. Fo
 Configuration property to specify the list of classes to exclude from scans. For example,
 `mp.openapi.scan.exclude.classes=com.xyz.MyClassC,com.xyz.MyClassD`
 
+[#scan-beanvalidation-config]
+`mp.openapi.scan.beanvalidation`::
+Configuration property to enable or disable the scanning and processing of Jakarta Bean Validation annotations. Defaults to `true`.
+
 `mp.openapi.servers`::
 Configuration property to specify the list of global servers that provide connectivity information. For example,
 `mp.openapi.servers=https://xyz.com/v1,https://abc.com/v1`
@@ -573,6 +577,48 @@ post:
 
 For more samples please see the https://github.com/eclipse/microprofile-open-api/wiki[MicroProfile Wiki].
 
+==== Jakarta Bean Validation Annotations
+
+In some cases, additional schema restrictions can be inferred from Jakarta Bean Validation annotations and used to enhance the generated OpenAPI document.
+
+Implementations are required to process Jakarta Bean Validation annotations and add the properties listed in the table below to the schema model when:
+* the annotation is applied to to an element for which a schema is generated and
+* the annotation and generated schema type are listed together in the table below and
+* the annotation has a `group` attribute which is empty or includes `jakarta.validation.groups.Default` and
+* the user has not set any of the relevant property values using other annotations and
+* processing of bean validation annotations has not been disabled <>
+
+|===
+| Annotation | Schema type | Schema properties to set
+| `@NotEmpty` | `string` | `minLength = 1`
+| `@NotEmpty` | `array` | `minItems = 1`
+| `@NotEmpty` | `object` | `minProperties = 1`
+| `@NotBlank` | `string` | `pattern = \S`
+| `@Size(min = a, max = b)` | `string`
+| `minLength = a` +
+`maxLenth = b`
+| `@Size(min = a, max = b)` | `array`
+| `minItems = a` +
+`maxItems = b`
+| `@Size(min = a, max = b)` | `object`
+| `minProperties = a` +
+`maxProperties = b`
+| `@DecimalMax(value = a)` | `number` or `integer` | `maximum = a`
+| `@DecimalMax(value = a, exclusive = false)` | `number` or `integer` | `maximum = a` +
+`exclusiveMaximum = true`
+| `@DecimalMin(value = a)` | `number` or `integer` | `minimum = a`
+| `@DecimalMin(value = a, exclusive = false)` | `number` or `integer` | `minimum = a` +
+`exclusiveMinimum = true`
+| `@Max(a)` | `number` or `integer` | `maximum = a`
+| `@Min(a)` | `number` or `integer` | `minimum = a`
+| `@Negative` | `number` or `integer` | `maximum = 0` +
+`exclusiveMaximum = true`
+| `@NegativeOrZero` | `number` or `integer` | `maximum = 0`
+| `@Positive` | `number` or `integer` | `minimum = 0` +
+`exclusiveMinimum = true`
+| `@PositiveOrZero` | `number` or `integer` | `minimum = 0`
+|===
+
 === Static OpenAPI files
 
 Application developers may wish to include a pre-generated OpenAPI document that
diff --git a/tck/pom.xml b/tck/pom.xml
index c3b48434e..cd3a2df8f 100644
--- a/tck/pom.xml
+++ b/tck/pom.xml
@@ -49,6 +49,12 @@
             3.0.0
             provided
         
+        
+            jakarta.validation
+            jakarta.validation-api
+            3.0.0
+            provided
+        
         
         
             org.testng
diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationApp.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationApp.java
new file mode 100644
index 000000000..ab183d4db
--- /dev/null
+++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationApp.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * 

+ * Licensed 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.eclipse.microprofile.openapi.apps.beanvalidation; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/") +public class BeanValidationApp extends Application { + +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java new file mode 100644 index 000000000..e5fa143e8 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *

+ * Licensed 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.eclipse.microprofile.openapi.apps.beanvalidation; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import jakarta.validation.groups.Default; + +public class BeanValidationData { + + @NotEmpty + private String notEmptyString; + + @NotEmpty + private List notEmptyList; + + @NotEmpty + private Map notEmptyMap; + + @NotBlank + private String notBlankString; + + @Size(min = 2, max = 7) + private String sizedString; + + @Size(min = 1, max = 10) + private List sizedList; + + @Size(min = 3, max = 5) + private Map sizedMap; + + @DecimalMax("1.5") + private BigDecimal maxDecimalInclusive; + + @DecimalMax(value = "1.5", inclusive = false) + private BigDecimal maxDecimalExclusive; + + @DecimalMin("3.25") + private BigDecimal minDecimalInclusive; + + @DecimalMin(value = "3.25", inclusive = false) + private BigDecimal minDecimalExclusive; + + @Max(5) + private int maxInt; + + @Min(7) + private int minInt; + + @Negative + private int negativeInt; + + @NegativeOrZero + private int negativeOrZeroInt; + + @Positive + private int positiveInt; + + @PositiveOrZero + private int positiveOrZeroInt; + + @Schema(minLength = 6) + @NotEmpty + private String overridenBySchemaAnnotation; + + @NotEmpty(groups = TestGroup.class) + private String nonDefaultGroup; + + @NotEmpty(groups = {Default.class, TestGroup.class}) + private String defaultAndOtherGroups; + + public String getNotEmptyString() { + return notEmptyString; + } + + public void setNotEmptyString(String notEmptyString) { + this.notEmptyString = notEmptyString; + } + + public List getNotEmptyList() { + return notEmptyList; + } + + public void setNotEmptyList(List notEmptyList) { + this.notEmptyList = notEmptyList; + } + + public Map getNotEmptyMap() { + return notEmptyMap; + } + + public void setNotEmptyMap(Map notEmptyMap) { + this.notEmptyMap = notEmptyMap; + } + + public String getNotBlankString() { + return notBlankString; + } + + public void setNotBlankString(String notBlankString) { + this.notBlankString = notBlankString; + } + + public String getSizedString() { + return sizedString; + } + + public void setSizedString(String sizedString) { + this.sizedString = sizedString; + } + + public List getSizedList() { + return sizedList; + } + + public void setSizedList(List sizedList) { + this.sizedList = sizedList; + } + + public Map getSizedMap() { + return sizedMap; + } + + public void setSizedMap(Map sizedMap) { + this.sizedMap = sizedMap; + } + + public BigDecimal getMaxDecimalInclusive() { + return maxDecimalInclusive; + } + + public void setMaxDecimalInclusive(BigDecimal maxDecimalInclusive) { + this.maxDecimalInclusive = maxDecimalInclusive; + } + + public BigDecimal getMaxDecimalExclusive() { + return maxDecimalExclusive; + } + + public void setMaxDecimalExclusive(BigDecimal maxDecimalExclusive) { + this.maxDecimalExclusive = maxDecimalExclusive; + } + + public BigDecimal getMinDecimalInclusive() { + return minDecimalInclusive; + } + + public void setMinDecimalInclusive(BigDecimal minDecimalInclusive) { + this.minDecimalInclusive = minDecimalInclusive; + } + + public BigDecimal getMinDecimalExclusive() { + return minDecimalExclusive; + } + + public void setMinDecimalExclusive(BigDecimal minDecimalExclusive) { + this.minDecimalExclusive = minDecimalExclusive; + } + + public int getMaxInt() { + return maxInt; + } + + public void setMaxInt(int maxInt) { + this.maxInt = maxInt; + } + + public int getMinInt() { + return minInt; + } + + public void setMinInt(int minInt) { + this.minInt = minInt; + } + + public int getNegativeInt() { + return negativeInt; + } + + public void setNegativeInt(int negativeInt) { + this.negativeInt = negativeInt; + } + + public int getNegativeOrZeroInt() { + return negativeOrZeroInt; + } + + public void setNegativeOrZeroInt(int negativeOrZeroInt) { + this.negativeOrZeroInt = negativeOrZeroInt; + } + + public int getPositiveInt() { + return positiveInt; + } + + public void setPositiveInt(int positiveInt) { + this.positiveInt = positiveInt; + } + + public int getPositiveOrZeroInt() { + return positiveOrZeroInt; + } + + public void setPositiveOrZeroInt(int positiveOrZeroInt) { + this.positiveOrZeroInt = positiveOrZeroInt; + } + + public String getOverridenBySchemaAnnotation() { + return overridenBySchemaAnnotation; + } + + public void setOverridenBySchemaAnnotation(String overridenBySchemaAnnotation) { + this.overridenBySchemaAnnotation = overridenBySchemaAnnotation; + } + + public String getNonDefaultGroup() { + return nonDefaultGroup; + } + + public void setNonDefaultGroup(String nonDefaultGroup) { + this.nonDefaultGroup = nonDefaultGroup; + } + + public String getDefaultAndOtherGroups() { + return defaultAndOtherGroups; + } + + public void setDefaultAndOtherGroups(String defaultAndOtherGroups) { + this.defaultAndOtherGroups = defaultAndOtherGroups; + } +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationResource.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationResource.java new file mode 100644 index 000000000..8d6a5b8d2 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationResource.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *

+ * Licensed 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.eclipse.microprofile.openapi.apps.beanvalidation; + +import jakarta.validation.Valid; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +public class BeanValidationResource { + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void test(@Valid BeanValidationData data) { + } +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/TestGroup.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/TestGroup.java new file mode 100644 index 000000000..06a890502 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/TestGroup.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *

+ * Licensed 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.eclipse.microprofile.openapi.apps.beanvalidation; + +/** + * A non-default bean validation constraint group for the bean validation test + */ +public interface TestGroup { +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationDisabledTest.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationDisabledTest.java new file mode 100644 index 000000000..acc8e5c3c --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationDisabledTest.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *

+ * Licensed 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.eclipse.microprofile.openapi.tck.beanvalidation; + +import static org.eclipse.microprofile.openapi.tck.beanvalidation.BeanValidationTest.assertProperty; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; + +import org.eclipse.microprofile.openapi.OASConfig; +import org.eclipse.microprofile.openapi.apps.beanvalidation.BeanValidationApp; +import org.eclipse.microprofile.openapi.tck.AppTestBase; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.Asset; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.testng.annotations.Test; + +import io.restassured.response.ValidatableResponse; + +public class BeanValidationDisabledTest extends AppTestBase { + + @Deployment + public static WebArchive buildApp() { + Asset config = new StringAsset(OASConfig.SCAN_BEANVALIDATION + "=false"); + + return ShrinkWrap.create(WebArchive.class, "beanValidation.war") + .addPackage(BeanValidationApp.class.getPackage()) + .addAsManifestResource(config, "microprofile-config.properties"); + } + + @Test(dataProvider = "formatProvider") + @RunAsClient + public void beanValidationScanningDisabledTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "notEmptyString", not(hasKey("minLength"))); + } + +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java new file mode 100644 index 000000000..50a90de27 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *

+ * Licensed 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.eclipse.microprofile.openapi.tck.beanvalidation; + +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; + +import org.eclipse.microprofile.openapi.apps.beanvalidation.BeanValidationApp; +import org.eclipse.microprofile.openapi.tck.AppTestBase; +import org.hamcrest.Matcher; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.testng.annotations.Test; + +import io.restassured.response.ValidatableResponse; + +public class BeanValidationTest extends AppTestBase { + + @Deployment + public static WebArchive buildApp() { + return ShrinkWrap.create(WebArchive.class, "beanValidation.war") + .addPackage(BeanValidationApp.class.getPackage()); + } + + @Test(dataProvider = "formatProvider") + @RunAsClient + public void notEmptyStringTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "notEmptyString", hasEntry("minLength", 1)); + assertProperty(vr, "notEmptyString", hasEntry("type", "string")); + } + + @Test(dataProvider = "formatProvider") + @RunAsClient + public void notEmptyListTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "notEmptyList", hasEntry("minItems", 1)); + assertProperty(vr, "notEmptyList", hasEntry("type", "array")); + } + + @Test(dataProvider = "formatProvider") + @RunAsClient + public void notEmptyMapTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "notEmptyMap", hasEntry("minProperties", 1)); + assertProperty(vr, "notEmptyMap", hasEntry("type", "object")); + } + + @Test(dataProvider = "formatProvider") + @RunAsClient + public void notBlankStringTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "notBlankString", hasEntry("pattern", "\\S")); + } + + @Test(dataProvider = "formatProvider") + @RunAsClient + public void sizedStringTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "sizedString", hasEntry("minLength", 2)); + assertProperty(vr, "sizedString", hasEntry("maxLength", 7)); + assertProperty(vr, "sizedString", hasEntry("type", "string")); + } + + @Test(dataProvider = "formatProvider") + public void sizedListTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "sizedList", hasEntry("minItems", 1)); + assertProperty(vr, "sizedList", hasEntry("maxItems", 10)); + assertProperty(vr, "sizedList", hasEntry("type", "array")); + } + + @Test(dataProvider = "formatProvider") + public void sizedMapTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "sizedMap", hasEntry("minProperties", 3)); + assertProperty(vr, "sizedMap", hasEntry("maxProperties", 5)); + assertProperty(vr, "sizedMap", hasEntry("type", "object")); + } + + @Test(dataProvider = "formatProvider") + public void maxDecimalInclusiveTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "maxDecimalInclusive", hasEntry("maximum", 1.5f)); + assertProperty(vr, "maxDecimalInclusive", hasEntry("type", "number")); + } + + @Test(dataProvider = "formatProvider") + public void maxDecimalExclusiveTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "maxDecimalExclusive", hasEntry("maximum", 1.5f)); + assertProperty(vr, "maxDecimalExclusive", hasEntry("exclusiveMaximum", true)); + assertProperty(vr, "maxDecimalExclusive", hasEntry("type", "number")); + } + + @Test(dataProvider = "formatProvider") + public void minDecimalInclusiveTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "minDecimalInclusive", hasEntry("minimum", 3.25f)); + assertProperty(vr, "minDecimalInclusive", hasEntry("type", "number")); + } + + @Test(dataProvider = "formatProvider") + public void minDecimalExclusiveTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "minDecimalExclusive", hasEntry("minimum", 3.25f)); + assertProperty(vr, "minDecimalExclusive", hasEntry("exclusiveMinimum", true)); + assertProperty(vr, "minDecimalExclusive", hasEntry("type", "number")); + } + + @Test(dataProvider = "formatProvider") + public void maxIntTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "maxInt", hasEntry("maximum", 5)); + } + + @Test(dataProvider = "formatProvider") + public void minIntTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "minInt", hasEntry("minimum", 7)); + } + + @Test(dataProvider = "formatProvider") + public void negativeIntTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "negativeInt", hasEntry("maximum", 0)); + assertProperty(vr, "negativeInt", hasEntry("exclusiveMaximum", true)); + } + + @Test(dataProvider = "formatProvider") + public void negativeOrZeroIntTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "negativeOrZeroInt", hasEntry("maximum", 0)); + } + + @Test(dataProvider = "formatProvider") + public void positiveIntTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "positiveInt", hasEntry("minimum", 0)); + assertProperty(vr, "positiveInt", hasEntry("exclusiveMinimum", true)); + } + + @Test(dataProvider = "formatProvider") + public void positiveOrZeroIntTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "positiveOrZeroInt", hasEntry("minimum", 0)); + } + + @Test(dataProvider = "formatProvider") + public void overridenBySchemaAnnotationTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "overridenBySchemaAnnotation", hasEntry("minLength", 6)); + } + + @Test(dataProvider = "formatProvider") + public void nonDefaultGroupTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "nonDefaultGroup", not(hasKey("minLength"))); + } + + @Test(dataProvider = "formatProvider") + public void defaultAndOtherGroupsTest(String format) { + ValidatableResponse vr = callEndpoint(format); + assertProperty(vr, "defaultAndOtherGroups", hasEntry("minLength", 1)); + } + + /** + * Asserts that a property from the test schema matches the given matcher + * + * @param vr + * the response + * @param propertyName + * the property to test + * @param matcher + * the matcher to assert + */ + public static void assertProperty(ValidatableResponse vr, String propertyName, Matcher matcher) { + String schemaPath = dereference(vr, "paths.'/'.post.requestBody", "content.'application/json'.schema"); + String propertyPath = schemaPath + ".properties." + propertyName; + vr.body(propertyPath, matcher); + } + +}