Skip to content

Commit

Permalink
Support advanced form submission
Browse files Browse the repository at this point in the history
  • Loading branch information
reda-alaoui committed Feb 27, 2024
1 parent 34c8ba3 commit 726b82c
Show file tree
Hide file tree
Showing 14 changed files with 2,354 additions and 12 deletions.
183 changes: 183 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/Form.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.cosium.hal_mock_mvc;

import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultActions;

/**
* @author Réda Housni Alaoui
*/
public class Form {

private final RequestExecutor requestExecutor;
private final ObjectMapper objectMapper;
private final Template template;

private final Map<String, ValidatedFormProperty<?>> propertyByName = new HashMap<>();

Form(RequestExecutor requestExecutor, ObjectMapper objectMapper, Template template) {
this.requestExecutor = requireNonNull(requestExecutor);
this.objectMapper = requireNonNull(objectMapper);
this.template = requireNonNull(template);
}

public Form withString(String propertyName, String value) throws Exception {
FormProperty<?> property =
new FormProperty<>(
String.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withBoolean(String propertyName, Boolean value) throws Exception {
FormProperty<?> property =
new FormProperty<>(
Boolean.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withInteger(String propertyName, Integer value) throws Exception {
FormProperty<?> property =
new FormProperty<>(
Integer.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withLong(String propertyName, Long value) throws Exception {
FormProperty<?> property =
new FormProperty<>(
Long.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withDouble(String propertyName, Double value) throws Exception {
FormProperty<?> property =
new FormProperty<>(
Double.class, propertyName, Optional.ofNullable(value).stream().toList(), false);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withStrings(String propertyName, List<String> value) throws Exception {
FormProperty<?> property = new FormProperty<>(String.class, propertyName, value, true);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withBooleans(String propertyName, List<Boolean> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Boolean.class, propertyName, value, true);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withIntegers(String propertyName, List<Integer> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Integer.class, propertyName, value, true);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withLongs(String propertyName, List<Long> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Long.class, propertyName, value, true);
propertyByName.put(property.name(), validate(property));
return this;
}

public Form withDoubles(String propertyName, List<Double> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Double.class, propertyName, value, true);
propertyByName.put(property.name(), validate(property));
return this;
}

public ResultActions submit() throws Exception {
String contentType = template.representation().contentType();
if (!MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
throw new UnsupportedOperationException(
"Expected content type is '%s'. For now, the only supported content type is '%s'."
.formatted(contentType, MediaType.APPLICATION_JSON_VALUE));
}

List<TemplateProperty> templateProperties =
template.representation().propertyByName().values().stream()
.map(
propertyRepresentation ->
new TemplateProperty(requestExecutor, objectMapper, propertyRepresentation))
.toList();

Map<String, Object> payload = new HashMap<>();

templateProperties.forEach(
property -> {
Object defaultValue = property.defaultValue().orElse(null);
if (defaultValue == null) {
return;
}
payload.put(property.name(), defaultValue);
});

List<String> expectedBadRequestReasons = new ArrayList<>();

propertyByName
.values()
.forEach(
formProperty ->
formProperty
.populateRequestPayload(payload)
.serverSideVerifiableErrorMessage()
.ifPresent(expectedBadRequestReasons::add));

templateProperties.stream()
.filter(TemplateProperty::isRequired)
.map(TemplateProperty::name)
.filter(propertyName -> payload.get(propertyName) == null)
.findFirst()
.map("Property '%s' is required but is missing"::formatted)
.ifPresent(expectedBadRequestReasons::add);

ResultActions resultActions = template.submit(objectMapper.writeValueAsString(payload));
if (expectedBadRequestReasons.isEmpty()) {
return resultActions;
}
int status = resultActions.andReturn().getResponse().getStatus();
HttpStatus.Series statusSeries = HttpStatus.Series.resolve(status);
if (statusSeries == HttpStatus.Series.CLIENT_ERROR) {
return resultActions;
}
throw new AssertionError(
"An http status code 400 was expected because of the following reasons: [%s]. Got http status code %s instead."
.formatted(String.join(",", expectedBadRequestReasons), status));
}

private ValidatedFormProperty<?> validate(FormProperty<?> property) throws Exception {
TemplatePropertyRepresentation representation = requireTemplate(property);
if (representation.readOnly()) {
throw new AssertionError(
"Cannot set value for read-only property '%s'".formatted(property.name()));
}
ValidatedFormProperty<?> validatedFormProperty =
new TemplateProperty(requestExecutor, objectMapper, representation).validate(property);
ValidatedFormProperty.ValidationError firstValidationError =
validatedFormProperty.firstValidationError();
if (firstValidationError != null && !firstValidationError.serverSideVerifiable()) {
throw new AssertionError(firstValidationError.reason());
}
return validatedFormProperty;
}

private TemplatePropertyRepresentation requireTemplate(FormProperty<?> property) {
TemplateRepresentation templateRepresentation = template.representation();
return Optional.ofNullable(templateRepresentation.propertyByName().get(property.name()))
.orElseThrow(
() -> new AssertionError("No property '%s' found.".formatted(property.name())));
}
}
70 changes: 70 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/FormProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.cosium.hal_mock_mvc;

import static java.util.Objects.requireNonNull;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* @author Réda Housni Alaoui
*/
record FormProperty<T>(Class<T> valueType, String name, List<T> values, boolean array) {

FormProperty {
if (!array && values.size() > 1) {
throw new IllegalArgumentException("Non array property can't hold more than 1 value.");
}
requireNonNull(valueType);
requireNonNull(name);
values = values.stream().filter(Objects::nonNull).toList();
}

public void populateRequestPayload(Map<String, Object> requestPayload) {
Object value;
if (array) {
value = values;
} else {
value = values.stream().findFirst().orElse(null);
}
requestPayload.put(name, value);
}

public boolean isNumberValueType() {
return Set.of(Integer.class, Long.class, Double.class).contains(valueType);
}

public List<Double> toDoubleValues() {
if (!isNumberValueType()) {
throw new IllegalArgumentException("%s is not a number type".formatted(valueType));
}
if (Integer.class.equals(valueType)) {
return values.stream()
.map(Integer.class::cast)
.map(
integer -> {
if (integer == null) {
return null;
}
return integer.doubleValue();
})
.toList();
} else if (Long.class.equals(valueType)) {
return values.stream()
.map(Long.class::cast)
.map(
aLong -> {
if (aLong == null) {
return null;
}
return aLong.doubleValue();
})
.toList();
} else if (Double.class.equals(valueType)) {
return values.stream().map(Double.class::cast).toList();
} else {
throw new IllegalArgumentException("Unexpected value type %s".formatted(valueType));
}
}
}
8 changes: 8 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static java.util.Objects.requireNonNull;

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

private final RequestExecutor requestExecutor;
private final ObjectMapper objectMapper;
private final String key;
private final TemplateRepresentation representation;

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

Template(
RequestExecutor requestExecutor,
ObjectMapper objectMapper,
String baseUri,
String key,
TemplateRepresentation representation) {
this.requestExecutor = requireNonNull(requestExecutor);
this.objectMapper = requireNonNull(objectMapper);
this.key = requireNonNull(key);
this.representation = requireNonNull(representation);

httpMethod = representation.method().toUpperCase();
target = URI.create(representation.target().orElse(baseUri));
}

public Form createForm() {
return new Form(requestExecutor, objectMapper, this);
}

@Override
public HalMockMvc createAndShift() throws Exception {
return createAndShift(null);
Expand Down
Loading

0 comments on commit 726b82c

Please sign in to comment.