Skip to content

Commit ac1e2a8

Browse files
committed
Test full combination of required parameter objects. Fixes springdoc#2787
Adds various test to prove that the setting of the `required` status on a collapsed `@ParameterObject` field in swagger matches either what's specifically overridden in the `Schema` annotation, or what the Java validator will enforce.
1 parent d34ab27 commit ac1e2a8

File tree

3 files changed

+459
-42
lines changed

3 files changed

+459
-42
lines changed

Diff for: springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/ParameterController.java

+43-34
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
* * * *
2222
* * *
2323
* *
24-
*
24+
*
2525
*/
2626
package test.org.springdoc.api.v30.app233;
2727

2828
import io.swagger.v3.oas.annotations.Parameter;
2929
import io.swagger.v3.oas.annotations.media.Schema;
30+
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
31+
import jakarta.validation.Valid;
3032
import jakarta.validation.constraints.NotNull;
3133
import org.springdoc.core.annotations.ParameterObject;
3234

@@ -36,51 +38,58 @@
3638
@RestController
3739
public class ParameterController {
3840

39-
@GetMapping("/hidden-parent")
40-
public void nestedParameterObjectWithHiddenParentField(@ParameterObject ParameterObjectWithHiddenField parameters) {
41+
@GetMapping("/hidden-parent")
42+
public void nestedParameterObjectWithHiddenParentField(@ParameterObject ParameterObjectWithHiddenField parameters) {
4143

42-
}
44+
}
4345

44-
public record ParameterObjectWithHiddenField(
45-
@Schema(hidden = true) NestedParameterObject schemaHiddenNestedParameterObject,
46-
@Parameter(hidden = true) NestedParameterObject parameterHiddenNestedParameterObject,
47-
NestedParameterObject visibleNestedParameterObject
48-
) {
46+
public record ParameterObjectWithHiddenField(
47+
@Schema(hidden = true) NestedParameterObject schemaHiddenNestedParameterObject,
48+
@Parameter(hidden = true) NestedParameterObject parameterHiddenNestedParameterObject,
49+
NestedParameterObject visibleNestedParameterObject
50+
) {
4951

50-
}
52+
}
5153

52-
public record NestedParameterObject(
53-
String parameterField) {
54-
}
54+
public record NestedParameterObject(
55+
String parameterField) {
56+
}
5557

56-
@GetMapping("/renamed-parent")
57-
public void nestedParameterObjectWithRenamedParentField(@ParameterObject ParameterObjectWithRenamedField parameters) {
58+
@GetMapping("/renamed-parent")
59+
public void nestedParameterObjectWithRenamedParentField(@ParameterObject ParameterObjectWithRenamedField parameters) {
5860

59-
}
61+
}
6062

61-
public record ParameterObjectWithRenamedField(
62-
@Schema(name = "schemaRenamed") NestedParameterObject schemaRenamedNestedParameterObject,
63-
@Parameter(name = "parameterRenamed") NestedParameterObject parameterRenamedNestedParameterObject,
64-
NestedParameterObject originalNameNestedParameterObject
65-
) {
63+
public record ParameterObjectWithRenamedField(
64+
@Schema(name = "schemaRenamed") NestedParameterObject schemaRenamedNestedParameterObject,
65+
@Parameter(name = "parameterRenamed") NestedParameterObject parameterRenamedNestedParameterObject,
66+
NestedParameterObject originalNameNestedParameterObject
67+
) {
6668

67-
}
69+
}
6870

69-
@GetMapping("/optional-parent")
70-
public void nestedParameterObjectWithOptionalParentField(@ParameterObject ParameterObjectWithOptionalField parameters) {
71+
@GetMapping("/optional-parent")
72+
public void nestedParameterObjectWithOptionalParentField(@Valid @ParameterObject MultiFieldParameterObject parameters) {
7173

72-
}
74+
}
7375

74-
public record ParameterObjectWithOptionalField(
75-
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) NestedRequiredParameterObject schemaNotRequiredNestedParameterObject,
76-
@Parameter NestedRequiredParameterObject parameterNotRequiredNestedParameterObject,
77-
@Parameter(required = true) NestedRequiredParameterObject requiredNestedParameterObject
78-
) {
76+
public record MultiFieldParameterObject(
77+
@Valid @Schema(requiredMode = RequiredMode.REQUIRED) @NotNull MultiFieldNestedParameterObject requiredNotNullParameterObject,
78+
@Valid @Schema(requiredMode = RequiredMode.REQUIRED) MultiFieldNestedParameterObject requiredNoValidationParameterObject,
79+
@Valid @Schema(requiredMode = RequiredMode.NOT_REQUIRED) @NotNull MultiFieldNestedParameterObject notRequiredNotNullParameterObject,
80+
@Valid @Schema(requiredMode = RequiredMode.NOT_REQUIRED) MultiFieldNestedParameterObject notRequiredNoValidationParameterObject,
81+
@Valid @NotNull MultiFieldNestedParameterObject noSchemaNotNullParameterObject,
82+
@Valid MultiFieldNestedParameterObject noSchemaNoValidationParameterObject) {
7983

80-
}
84+
}
8185

82-
public record NestedRequiredParameterObject(
83-
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotNull String requiredParameterField) {
84-
}
86+
public record MultiFieldNestedParameterObject (
87+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotNull String requiredNotNullField,
88+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String requiredNoValidationField,
89+
@Schema(requiredMode = RequiredMode.NOT_REQUIRED) @NotNull String notRequiredNotNullField,
90+
@Schema(requiredMode = RequiredMode.NOT_REQUIRED) String notRequiredNoValidationField,
91+
@NotNull String noSchemaNotNullField,
92+
String noSchemaNoValidationField) {
93+
}
8594

8695
}

Diff for: springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/SpringDocApp233Test.java

+149-5
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,160 @@
2626

2727
package test.org.springdoc.api.v30.app233;
2828

29+
import java.util.Collection;
30+
import java.util.List;
31+
import java.util.Optional;
32+
import java.util.Set;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
35+
36+
import com.jayway.jsonpath.JsonPath;
37+
import net.minidev.json.JSONArray;
38+
import org.junit.jupiter.api.Test;
39+
import org.junit.jupiter.params.ParameterizedTest;
40+
import org.junit.jupiter.params.provider.CsvSource;
41+
import org.springdoc.core.utils.Constants;
2942
import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
3043

3144
import org.springframework.boot.autoconfigure.SpringBootApplication;
45+
import org.springframework.test.web.servlet.MvcResult;
46+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
47+
import org.springframework.validation.BindingResult;
48+
import org.springframework.web.bind.MethodArgumentNotValidException;
49+
50+
import static org.assertj.core.api.Assertions.assertThat;
51+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
52+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
3253

3354
/**
34-
* @author bnasslahsen
55+
* @author bnasslahsen, michael.clarke
3556
*/
36-
public class SpringDocApp233Test extends AbstractSpringDocV30Test {
57+
class SpringDocApp233Test extends AbstractSpringDocV30Test {
58+
59+
@CsvSource({"requiredNotNullParameterObject.requiredNotNullField, true",
60+
"requiredNotNullParameterObject.requiredNoValidationField, true",
61+
"requiredNotNullParameterObject.notRequiredNotNullField, false",
62+
"requiredNotNullParameterObject.notRequiredNoValidationField, false",
63+
"requiredNotNullParameterObject.noSchemaNotNullField, true",
64+
"requiredNotNullParameterObject.noSchemaNoValidationField, false",
65+
"requiredNoValidationParameterObject.requiredNotNullField, true",
66+
"requiredNoValidationParameterObject.requiredNoValidationField, true",
67+
"requiredNoValidationParameterObject.notRequiredNotNullField, false",
68+
"requiredNoValidationParameterObject.notRequiredNoValidationField, false",
69+
"requiredNoValidationParameterObject.noSchemaNotNullField, true",
70+
"requiredNoValidationParameterObject.noSchemaNoValidationField, false",
71+
"notRequiredNotNullParameterObject.requiredNotNullField, false",
72+
"notRequiredNotNullParameterObject.requiredNoValidationField, false",
73+
"notRequiredNotNullParameterObject.notRequiredNotNullField, false",
74+
"notRequiredNotNullParameterObject.notRequiredNoValidationField, false",
75+
"notRequiredNotNullParameterObject.noSchemaNotNullField, false",
76+
"notRequiredNotNullParameterObject.noSchemaNoValidationField, false",
77+
"notRequiredNoValidationParameterObject.requiredNotNullField, false",
78+
"notRequiredNoValidationParameterObject.requiredNoValidationField, false",
79+
"notRequiredNoValidationParameterObject.notRequiredNotNullField, false",
80+
"notRequiredNoValidationParameterObject.notRequiredNoValidationField, false",
81+
"notRequiredNoValidationParameterObject.noSchemaNotNullField, false",
82+
"notRequiredNoValidationParameterObject.noSchemaNoValidationField, false",
83+
"noSchemaNotNullParameterObject.requiredNotNullField, true",
84+
"noSchemaNotNullParameterObject.requiredNoValidationField, true",
85+
"noSchemaNotNullParameterObject.notRequiredNotNullField, false",
86+
"noSchemaNotNullParameterObject.notRequiredNoValidationField, false",
87+
"noSchemaNotNullParameterObject.noSchemaNotNullField, true",
88+
"noSchemaNotNullParameterObject.noSchemaNoValidationField, false",
89+
"noSchemaNoValidationParameterObject.requiredNotNullField, false",
90+
"noSchemaNoValidationParameterObject.requiredNoValidationField, false",
91+
"noSchemaNoValidationParameterObject.notRequiredNotNullField, false",
92+
"noSchemaNoValidationParameterObject.notRequiredNoValidationField, false",
93+
"noSchemaNoValidationParameterObject.noSchemaNotNullField, false",
94+
"noSchemaNoValidationParameterObject.noSchemaNoValidationField, false"})
95+
@ParameterizedTest
96+
void shouldHaveCorrectRequireStatus(String field, String required) throws Exception {
97+
MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk()).andReturn();
98+
String result = mockMvcResult.getResponse().getContentAsString();
99+
100+
String requiredMode = ((JSONArray) JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[?(@.name == '" + field + "')].required")).get(0).toString();
101+
assertThat(requiredMode).isEqualTo(required);
102+
}
103+
104+
@Test
105+
void verifySwaggerFieldRequirementsMatchJavaValidation() throws Exception {
106+
MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk()).andReturn();
107+
String result = mockMvcResult.getResponse().getContentAsString();
108+
109+
JSONArray allFieldsJsonArray = JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[*].name");
110+
List<String> allFields = allFieldsJsonArray.stream().map(Object::toString).toList();
111+
112+
// check we get no validation failures when all mandatory fields are present
113+
verifySwaggerFieldRequirementsMatchJavaValidation(allFields, List.of());
114+
115+
JSONArray mandatoryFieldsJsonArray = JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[?(@.required == true)].name");
116+
List<String> mandatoryFields = mandatoryFieldsJsonArray.stream().map(Object::toString).toList();
117+
118+
// check validation failures when each individual mandatory field is missing
119+
for (String mandatoryField : mandatoryFields) {
120+
List<String> filteredFields = allFields.stream()
121+
.filter(field -> !field.equals(mandatoryField))
122+
.toList();
123+
124+
List<String> expectedErrors = Stream.of(mandatoryField)
125+
// Fields using Swagger annotations to drive required status but not using Java validation to enforce it so don't cause validation errors
126+
.filter(field -> !field.endsWith("requiredNullableField"))
127+
.filter(field -> !field.endsWith("requiredNoValidationField"))
128+
// the error is returned prefixed with the query parameter name, so add it to the expected error message
129+
.map(field -> "multiFieldParameterObject." + field)
130+
.toList();
131+
verifySwaggerFieldRequirementsMatchJavaValidation(filteredFields, expectedErrors);
132+
}
133+
134+
135+
JSONArray nonMandatoryFieldsJsonArray = JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[?(@.required == false)].name");
136+
List<String> nonMandatoryFields = nonMandatoryFieldsJsonArray.stream().map(Object::toString).toList();
137+
138+
// check validation failures for any individual non-mandatory fields being missed
139+
for (String nonMandatoryField : nonMandatoryFields) {
140+
List<String> filteredFields = allFields.stream()
141+
.filter(field -> !field.equals(nonMandatoryField))
142+
.toList();
143+
144+
List<String> expectedErrors = Stream.of(nonMandatoryField)
145+
// Fields that are mandatory but either have nullable parent fields so are excluded in swagger or are marked as not required so do cause validation errors
146+
.filter(field -> field.endsWith("NotNullField"))
147+
// the error is returned prefixed with the query parameter name, so add it to the expected error message
148+
.map(field -> "multiFieldParameterObject." + field)
149+
.toList();
150+
verifySwaggerFieldRequirementsMatchJavaValidation(filteredFields, expectedErrors);
151+
}
152+
}
153+
154+
private void verifySwaggerFieldRequirementsMatchJavaValidation(Collection<String> requestFields, Collection<String> expectedErrorFields) throws Exception {
155+
MockHttpServletRequestBuilder request = get("/optional-parent");
156+
for (String mandatoryField : requestFields) {
157+
request.queryParam(mandatoryField, mandatoryField + ".value");
158+
}
159+
160+
mockMvc.perform(request)
161+
.andExpect(result -> {
162+
163+
Set<String> errorFields = Optional.ofNullable(result.getResolvedException())
164+
.map(MethodArgumentNotValidException.class::cast)
165+
.map(MethodArgumentNotValidException::getBindingResult)
166+
.map(BindingResult::getFieldErrors)
167+
.stream()
168+
.flatMap(Collection::stream)
169+
.map(field -> field.getObjectName() + "." + field.getField())
170+
.collect(Collectors.toSet());
171+
172+
173+
assertThat(errorFields).containsExactlyElementsOf(expectedErrorFields);
174+
175+
assertThat(result.getResponse().getStatus()).isEqualTo(expectedErrorFields.isEmpty() ? 200 : 400);
176+
});
177+
}
178+
179+
180+
@SpringBootApplication
181+
static class SpringDocTestApp {
182+
183+
}
37184

38-
@SpringBootApplication
39-
static class SpringDocTestApp {
40-
}
41185
}

0 commit comments

Comments
 (0)