Skip to content

Commit cac7ce6

Browse files
authored
Support advanced form submission (#20)
1 parent 34c8ba3 commit cac7ce6

File tree

14 files changed

+2620
-12
lines changed

14 files changed

+2620
-12
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.cosium.hal_mock_mvc;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import java.util.ArrayList;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.MediaType;
13+
import org.springframework.test.web.servlet.ResultActions;
14+
15+
/**
16+
* @author Réda Housni Alaoui
17+
*/
18+
public class Form {
19+
20+
private final RequestExecutor requestExecutor;
21+
private final ObjectMapper objectMapper;
22+
private final Template template;
23+
24+
private final Map<String, ValidatedFormProperty<?>> propertyByName = new HashMap<>();
25+
26+
Form(RequestExecutor requestExecutor, ObjectMapper objectMapper, Template template) {
27+
this.requestExecutor = requireNonNull(requestExecutor);
28+
this.objectMapper = requireNonNull(objectMapper);
29+
this.template = requireNonNull(template);
30+
}
31+
32+
public Form withString(String propertyName, String value) throws Exception {
33+
FormProperty<?> property =
34+
new FormProperty<>(
35+
String.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
36+
propertyByName.put(property.name(), validate(property));
37+
return this;
38+
}
39+
40+
public Form withBoolean(String propertyName, Boolean value) throws Exception {
41+
FormProperty<?> property =
42+
new FormProperty<>(
43+
Boolean.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
44+
propertyByName.put(property.name(), validate(property));
45+
return this;
46+
}
47+
48+
public Form withInteger(String propertyName, Integer value) throws Exception {
49+
FormProperty<?> property =
50+
new FormProperty<>(
51+
Integer.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
52+
propertyByName.put(property.name(), validate(property));
53+
return this;
54+
}
55+
56+
public Form withLong(String propertyName, Long value) throws Exception {
57+
FormProperty<?> property =
58+
new FormProperty<>(
59+
Long.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
60+
propertyByName.put(property.name(), validate(property));
61+
return this;
62+
}
63+
64+
public Form withDouble(String propertyName, Double value) throws Exception {
65+
FormProperty<?> property =
66+
new FormProperty<>(
67+
Double.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
68+
propertyByName.put(property.name(), validate(property));
69+
return this;
70+
}
71+
72+
public Form withStrings(String propertyName, List<String> value) throws Exception {
73+
FormProperty<?> property = new FormProperty<>(String.class, propertyName, value, true);
74+
propertyByName.put(property.name(), validate(property));
75+
return this;
76+
}
77+
78+
public Form withBooleans(String propertyName, List<Boolean> value) throws Exception {
79+
FormProperty<?> property = new FormProperty<>(Boolean.class, propertyName, value, true);
80+
propertyByName.put(property.name(), validate(property));
81+
return this;
82+
}
83+
84+
public Form withIntegers(String propertyName, List<Integer> value) throws Exception {
85+
FormProperty<?> property = new FormProperty<>(Integer.class, propertyName, value, true);
86+
propertyByName.put(property.name(), validate(property));
87+
return this;
88+
}
89+
90+
public Form withLongs(String propertyName, List<Long> value) throws Exception {
91+
FormProperty<?> property = new FormProperty<>(Long.class, propertyName, value, true);
92+
propertyByName.put(property.name(), validate(property));
93+
return this;
94+
}
95+
96+
public Form withDoubles(String propertyName, List<Double> value) throws Exception {
97+
FormProperty<?> property = new FormProperty<>(Double.class, propertyName, value, true);
98+
propertyByName.put(property.name(), validate(property));
99+
return this;
100+
}
101+
102+
public ResultActions submit() throws Exception {
103+
String contentType = template.representation().contentType();
104+
if (!MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
105+
throw new UnsupportedOperationException(
106+
"Expected content type is '%s'. For now, the only supported content type is '%s'."
107+
.formatted(contentType, MediaType.APPLICATION_JSON_VALUE));
108+
}
109+
110+
List<TemplateProperty> templateProperties =
111+
template.representation().propertyByName().values().stream()
112+
.map(
113+
propertyRepresentation ->
114+
new TemplateProperty(requestExecutor, objectMapper, propertyRepresentation))
115+
.toList();
116+
117+
Map<String, Object> payload = new HashMap<>();
118+
119+
templateProperties.forEach(
120+
property -> {
121+
Object defaultValue = property.defaultValue().orElse(null);
122+
if (defaultValue == null) {
123+
return;
124+
}
125+
payload.put(property.name(), defaultValue);
126+
});
127+
128+
List<String> expectedBadRequestReasons = new ArrayList<>();
129+
130+
propertyByName
131+
.values()
132+
.forEach(
133+
formProperty ->
134+
formProperty
135+
.populateRequestPayload(payload)
136+
.serverSideVerifiableErrorMessage()
137+
.ifPresent(expectedBadRequestReasons::add));
138+
139+
templateProperties.stream()
140+
.filter(TemplateProperty::isRequired)
141+
.map(TemplateProperty::name)
142+
.filter(propertyName -> payload.get(propertyName) == null)
143+
.findFirst()
144+
.map("Property '%s' is required but is missing"::formatted)
145+
.ifPresent(expectedBadRequestReasons::add);
146+
147+
ResultActions resultActions = template.submit(objectMapper.writeValueAsString(payload));
148+
if (expectedBadRequestReasons.isEmpty()) {
149+
return resultActions;
150+
}
151+
int status = resultActions.andReturn().getResponse().getStatus();
152+
HttpStatus.Series statusSeries = HttpStatus.Series.resolve(status);
153+
if (statusSeries == HttpStatus.Series.CLIENT_ERROR) {
154+
return resultActions;
155+
}
156+
throw new AssertionError(
157+
"An http status code 400 was expected because of the following reasons: [%s]. Got http status code %s instead."
158+
.formatted(String.join(",", expectedBadRequestReasons), status));
159+
}
160+
161+
private ValidatedFormProperty<?> validate(FormProperty<?> property) throws Exception {
162+
TemplatePropertyRepresentation representation = requireTemplate(property);
163+
if (representation.readOnly()) {
164+
throw new AssertionError(
165+
"Cannot set value for read-only property '%s'".formatted(property.name()));
166+
}
167+
ValidatedFormProperty<?> validatedFormProperty =
168+
new TemplateProperty(requestExecutor, objectMapper, representation).validate(property);
169+
ValidatedFormProperty.ValidationError firstValidationError =
170+
validatedFormProperty.firstValidationError();
171+
if (firstValidationError != null && !firstValidationError.serverSideVerifiable()) {
172+
throw new AssertionError(firstValidationError.reason());
173+
}
174+
return validatedFormProperty;
175+
}
176+
177+
private TemplatePropertyRepresentation requireTemplate(FormProperty<?> property) {
178+
TemplateRepresentation templateRepresentation = template.representation();
179+
return Optional.ofNullable(templateRepresentation.propertyByName().get(property.name()))
180+
.orElseThrow(
181+
() -> new AssertionError("No property '%s' found.".formatted(property.name())));
182+
}
183+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.cosium.hal_mock_mvc;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
import java.util.Set;
9+
10+
/**
11+
* @author Réda Housni Alaoui
12+
*/
13+
record FormProperty<T>(Class<T> valueType, String name, List<T> values, boolean array) {
14+
15+
FormProperty {
16+
if (!array && values.size() > 1) {
17+
throw new IllegalArgumentException("Non array property can't hold more than 1 value.");
18+
}
19+
requireNonNull(valueType);
20+
requireNonNull(name);
21+
values = values.stream().filter(Objects::nonNull).toList();
22+
}
23+
24+
public void populateRequestPayload(Map<String, Object> requestPayload) {
25+
Object value;
26+
if (array) {
27+
value = values;
28+
} else {
29+
value = values.stream().findFirst().orElse(null);
30+
}
31+
requestPayload.put(name, value);
32+
}
33+
34+
public boolean isNumberValueType() {
35+
return Set.of(Integer.class, Long.class, Double.class).contains(valueType);
36+
}
37+
38+
public List<Double> toDoubleValues() {
39+
if (!isNumberValueType()) {
40+
throw new IllegalArgumentException("%s is not a number type".formatted(valueType));
41+
}
42+
if (Integer.class.equals(valueType)) {
43+
return values.stream()
44+
.map(Integer.class::cast)
45+
.map(
46+
integer -> {
47+
if (integer == null) {
48+
return null;
49+
}
50+
return integer.doubleValue();
51+
})
52+
.toList();
53+
} else if (Long.class.equals(valueType)) {
54+
return values.stream()
55+
.map(Long.class::cast)
56+
.map(
57+
aLong -> {
58+
if (aLong == null) {
59+
return null;
60+
}
61+
return aLong.doubleValue();
62+
})
63+
.toList();
64+
} else if (Double.class.equals(valueType)) {
65+
return values.stream().map(Double.class::cast).toList();
66+
} else {
67+
throw new IllegalArgumentException("Unexpected value type %s".formatted(valueType));
68+
}
69+
}
70+
}

core/src/main/java/com/cosium/hal_mock_mvc/Template.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.util.Objects.requireNonNull;
44

5+
import com.fasterxml.jackson.databind.ObjectMapper;
56
import java.net.URI;
67
import org.springframework.test.web.servlet.ResultActions;
78
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -13,6 +14,7 @@
1314
public class Template implements SubmittableTemplate {
1415

1516
private final RequestExecutor requestExecutor;
17+
private final ObjectMapper objectMapper;
1618
private final String key;
1719
private final TemplateRepresentation representation;
1820

@@ -21,17 +23,23 @@ public class Template implements SubmittableTemplate {
2123

2224
Template(
2325
RequestExecutor requestExecutor,
26+
ObjectMapper objectMapper,
2427
String baseUri,
2528
String key,
2629
TemplateRepresentation representation) {
2730
this.requestExecutor = requireNonNull(requestExecutor);
31+
this.objectMapper = requireNonNull(objectMapper);
2832
this.key = requireNonNull(key);
2933
this.representation = requireNonNull(representation);
3034

3135
httpMethod = representation.method().toUpperCase();
3236
target = URI.create(representation.target().orElse(baseUri));
3337
}
3438

39+
public Form createForm() {
40+
return new Form(requestExecutor, objectMapper, this);
41+
}
42+
3543
@Override
3644
public HalMockMvc createAndShift() throws Exception {
3745
return createAndShift(null);

0 commit comments

Comments
 (0)