Skip to content

Commit

Permalink
form: allow to bypass "client-side" validation (#21)
Browse files Browse the repository at this point in the history
Co-authored-by: Réda Housni Alaoui <[email protected]>
  • Loading branch information
beuss and reda-alaoui authored Sep 5, 2024
1 parent 8822910 commit 0a33a15
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 31 deletions.
88 changes: 58 additions & 30 deletions core/src/main/java/com/cosium/hal_mock_mvc/Form.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultActions;
Expand All @@ -29,73 +30,93 @@ public class Form {
this.template = requireNonNull(template);
}

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

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

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

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

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

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

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

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

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

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

Expand Down Expand Up @@ -167,9 +188,23 @@ public ResultActions submit() throws Exception {
.formatted(String.join(",", expectedBadRequestReasons), status));
}

private ValidatedFormProperty<?> validate(FormProperty<?> property) throws Exception {
TemplatePropertyRepresentation representation = requireTemplate(property);
if (representation.readOnly()) {
private ValidatedFormProperty<?> validate(
FormProperty<?> property, PropertyValidationOption... validationOptions) throws Exception {
Set<PropertyValidationOption> validationOptionSet =
Optional.ofNullable(validationOptions).map(Set::of).orElse(Set.of());
TemplatePropertyRepresentation representation =
template.representation().propertyByName().get(property.name());
if (representation == null) {
if (!validationOptionSet.contains(
PropertyValidationOption.Immediate.DO_NOT_FAIL_IF_NOT_DECLARED)) {
throw new AssertionError("No property '%s' found.".formatted(property.name()));
}
return ValidatedFormProperty.markAsValid(property);
}

if (representation.readOnly()
&& !validationOptionSet.contains(
PropertyValidationOption.Immediate.DO_NOT_FAIL_IF_DECLARED_READ_ONLY)) {
throw new AssertionError(
"Cannot set value for read-only property '%s'".formatted(property.name()));
}
Expand All @@ -182,11 +217,4 @@ private ValidatedFormProperty<?> validate(FormProperty<?> property) throws Excep
}
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())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.cosium.hal_mock_mvc;

/**
* @author Sébastien Le Ray
* @author Réda Housni Alaoui
*/
public sealed interface PropertyValidationOption permits PropertyValidationOption.Immediate {
enum Immediate implements PropertyValidationOption {
/** Do not fail if writing to a property not declared by the template */
DO_NOT_FAIL_IF_NOT_DECLARED,
/** Do not fail if writing to a property declared as read-only by the template */
DO_NOT_FAIL_IF_DECLARED_READ_ONLY
}
}
84 changes: 83 additions & 1 deletion core/src/test/java/com/cosium/hal_mock_mvc/FormTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cosium.hal_mock_mvc;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
Expand Down Expand Up @@ -325,7 +326,7 @@ void test6(String halFormType) throws Exception {

@ParameterizedTest
@ValueSource(strings = {"month", "week", "number"})
@DisplayName("User cannot pass a number value when the type is only compatible with number")
@DisplayName("User can pass a number value when the type is only compatible with number")
void test7(String halFormType) throws Exception {
myController.getResponseToSend =
JSON.std
Expand Down Expand Up @@ -1761,6 +1762,87 @@ void test37() throws Exception {
.isEqualTo("baz");
}

@Test
@DisplayName("User can force pass an unknown property")
void test38() throws Exception {
myController.getResponseToSend =
JSON.std
.composeString()
.startObject()
.startObjectField("_links")
.startObjectField("self")
.put("href", "http://localhost/form-test:put")
.end()
.end()
.startObjectField("_templates")
.startObjectField("default")
.put("method", "PUT")
.startArrayField("properties")
.end()
.end()
.end()
.end()
.finish();

Form form =
HalMockMvc.builder(mockMvc)
.baseUri(linkTo(methodOn(MyController.class).get()).toUri())
.build()
.follow()
.templates()
.byKey("default")
.createForm();

assertThatCode(
() ->
form.withString(
"foo", "foo", PropertyValidationOption.Immediate.DO_NOT_FAIL_IF_NOT_DECLARED))
.doesNotThrowAnyException();
}

@Test
@DisplayName("User can force a readonly property")
void test39() throws Exception {
myController.getResponseToSend =
JSON.std
.composeString()
.startObject()
.startObjectField("_links")
.startObjectField("self")
.put("href", "http://localhost/form-test:put")
.end()
.end()
.startObjectField("_templates")
.startObjectField("default")
.put("method", "PUT")
.startArrayField("properties")
.startObject()
.put("name", "foo")
.put("readOnly", true)
.end()
.end()
.end()
.end()
.end()
.finish();

Form form =
HalMockMvc.builder(mockMvc)
.baseUri(linkTo(methodOn(MyController.class).get()).toUri())
.build()
.follow()
.templates()
.byKey("default")
.createForm();
assertThatCode(
() ->
form.withString(
"foo",
"foo",
PropertyValidationOption.Immediate.DO_NOT_FAIL_IF_DECLARED_READ_ONLY))
.doesNotThrowAnyException();
}

@Controller
public static class MyController {

Expand Down

0 comments on commit 0a33a15

Please sign in to comment.