Skip to content

Commit 57f12b4

Browse files
chore(validation): rework dto validation layer (#66)
* chore(attribute): add improved value validation * chore(font): rework parameter validation * chore(soundsoruce): add validation layer * chore(notification): add new validation layer * chore(sound): add validation to the SoundEventDTO file * chore(item): add validation layer * chore(deps): add new dependencies for the validation testing * chore(attribute): add missing validation annotation to the uiName field * chore(tests): add test base class for validation tess * fix(tests): improve assertNoViolation method * chore(tests): add different test implementation for each dto class * chore(validation): improve annotation usage * fix(deps): improve version naming and usage --------- Co-authored-by: theEvilReaper <[email protected]>
1 parent f410f59 commit 57f12b4

File tree

15 files changed

+752
-38
lines changed

15 files changed

+752
-38
lines changed

build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,19 @@ dependencies {
5252

5353
testImplementation(mn.junit.jupiter.api)
5454
testImplementation(mn.junit.jupiter.params)
55-
testRuntimeOnly(mn.junit.jupiter.engine)
5655
testImplementation(mn.testcontainers.core)
5756
testImplementation(mn.testcontainers.mariadb)
5857
testImplementation(mn.micronaut.test.rest.assured)
58+
testImplementation(mn.micronaut.validation)
5959
testImplementation(mn.micronaut.test.resources.extensions.core)
6060
testImplementation(mn.micronaut.test.resources.extensions.junit.platform)
6161
// Faker library for JUnit tests
6262
testImplementation(libs.testcontainers.junit)
6363
testImplementation(libs.datafaker)
64+
testImplementation(libs.hibernate.validator)
65+
testImplementation(libs.jakarta.validation)
66+
67+
testRuntimeOnly(mn.junit.jupiter.engine)
6468
}
6569

6670
application {

settings.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,18 @@ dependencyResolutionManagement {
3737
version("uuid.creator", "6.1.1")
3838
version("datafaker", "2.4.2")
3939
version("jetbrains.annotation", "26.0.2")
40+
version("hibernate.validator", "9.0.1.Final")
41+
version("jakarta.validation", "3.1.1")
4042

4143
library("uuid.creator", "com.github.f4b6a3", "uuid-creator").versionRef("uuid.creator")
4244
library("vulpes.api", "net.onelitefeather", "vulpes-model").versionRef("vulpes.model")
4345
library("jetbrains.annotation", "org.jetbrains", "annotations").versionRef("jetbrains.annotation")
4446
library("datafaker", "net.datafaker", "datafaker").versionRef("datafaker")
4547
library("testcontainers.junit", "org.testcontainers", "junit-jupiter").withoutVersion()
4648

49+
library("hibernate.validator", "org.hibernate.validator", "hibernate-validator").versionRef("hibernate.validator")
50+
library("jakarta.validation", "jakarta.validation", "jakarta.validation-api").versionRef("jakarta.validation")
51+
4752
plugin("micronaut.application", "io.micronaut.application").versionRef("micronaut")
4853
plugin("micronaut.aot", "io.micronaut.aot").versionRef("micronaut")
4954
plugin("micronaut.test-resources", "io.micronaut.test-resources").versionRef("micronaut")

src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelDTO.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import io.micronaut.core.annotation.Introspected;
44
import io.micronaut.serde.annotation.Serdeable;
55
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
7+
import jakarta.validation.constraints.Positive;
8+
import jakarta.validation.constraints.PositiveOrZero;
69
import net.onelitefeather.vulpes.api.model.AttributeEntity;
710

811
import java.util.UUID;
@@ -12,10 +15,10 @@
1215
@Introspected
1316
public record AttributeModelDTO(
1417
@Schema(description = "ID of the attribute", requiredMode = Schema.RequiredMode.NOT_REQUIRED) UUID id,
15-
@Schema(description = "The name for the ui", requiredMode = Schema.RequiredMode.REQUIRED) String uiName,
16-
@Schema(description = "The name which represents the variable after the generation", requiredMode = Schema.RequiredMode.REQUIRED) String variableName,
17-
@Schema(description = "Default value of the attribute", requiredMode = Schema.RequiredMode.REQUIRED) double defaultValue,
18-
@Schema(description = "Maximum value of the attribute", requiredMode = Schema.RequiredMode.REQUIRED) double maximumValue
18+
@Schema(description = "The name for the ui", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String uiName,
19+
@Schema(description = "The name which represents the variable after the generation", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String variableName,
20+
@Schema(description = "Default value of the attribute", requiredMode = Schema.RequiredMode.REQUIRED) @PositiveOrZero double defaultValue,
21+
@Schema(description = "Maximum value of the attribute", requiredMode = Schema.RequiredMode.REQUIRED) @Positive double maximumValue
1922
) {
2023
public AttributeEntity toAttributeModel() {
2124
return new AttributeEntity(id, uiName, variableName, defaultValue, maximumValue);

src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelDTO.java

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

33
import io.micronaut.serde.annotation.Serdeable;
44
import io.swagger.v3.oas.annotations.media.Schema;
5+
import jakarta.annotation.Nullable;
6+
import jakarta.validation.constraints.NotBlank;
57
import jakarta.validation.constraints.NotNull;
8+
import jakarta.validation.constraints.PositiveOrZero;
69
import net.onelitefeather.vulpes.api.model.FontEntity;
710

811
import java.util.List;
@@ -12,14 +15,14 @@
1215
@Serdeable
1316
public record FontModelDTO(
1417
@Schema(description = "ID of the mode", requiredMode = Schema.RequiredMode.NOT_REQUIRED) UUID id,
15-
@Schema(description = "Model Name for the ui", requiredMode = Schema.RequiredMode.REQUIRED) String uiName,
16-
@Schema(description = "Name in the UI", requiredMode = Schema.RequiredMode.REQUIRED) String variableName,
17-
@Schema(description = "Which provider should be used", requiredMode = Schema.RequiredMode.REQUIRED) String provider,
18-
@Schema(description = "Internal mapper variable", requiredMode = Schema.RequiredMode.REQUIRED) String mapper,
19-
@Schema(description = "The path to the texture", requiredMode = Schema.RequiredMode.REQUIRED) String texturePath,
20-
@Schema(description = "The comment", requiredMode = Schema.RequiredMode.REQUIRED) String comment,
21-
@Schema(description = "The ascent property", requiredMode = Schema.RequiredMode.REQUIRED) int ascent,
22-
@Schema(description = "The height property", requiredMode = Schema.RequiredMode.REQUIRED) int height,
18+
@Schema(description = "Model Name for the ui", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String uiName,
19+
@Schema(description = "Name in the UI", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String variableName,
20+
@Schema(description = "Which provider should be used", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String provider,
21+
@Schema(description = "Internal mapper variable", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String mapper,
22+
@Schema(description = "The path to the texture", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String texturePath,
23+
@Schema(description = "The comment", requiredMode = Schema.RequiredMode.REQUIRED) @Nullable String comment,
24+
@Schema(description = "The ascent property", requiredMode = Schema.RequiredMode.REQUIRED) @PositiveOrZero int ascent,
25+
@Schema(description = "The height property", requiredMode = Schema.RequiredMode.REQUIRED) @PositiveOrZero int height,
2326
@Schema(description = "The chars which are overwritten", requiredMode = Schema.RequiredMode.NOT_REQUIRED) List<String> chars
2427
) {
2528

src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelDTO.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import io.micronaut.core.annotation.Introspected;
44
import io.micronaut.serde.annotation.Serdeable;
55
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
67
import jakarta.validation.constraints.NotNull;
8+
import jakarta.validation.constraints.Positive;
9+
import jakarta.validation.constraints.PositiveOrZero;
710
import net.onelitefeather.vulpes.api.model.ItemEntity;
811

912
import java.util.List;
@@ -26,14 +29,14 @@
2629
@Serdeable
2730
public record ItemModelDTO(
2831
@Schema(description = "ID of the Model", requiredMode = Schema.RequiredMode.NOT_REQUIRED) UUID id,
29-
@Schema(description = "Name in the UI", requiredMode = Schema.RequiredMode.REQUIRED) String uiName,
30-
@Schema(description = "Variable name for the entity", requiredMode = Schema.RequiredMode.REQUIRED) String variableName,
32+
@Schema(description = "Name in the UI", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String uiName,
33+
@Schema(description = "Variable name for the entity", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String variableName,
3134
@Schema(description = "Internal description of the item", requiredMode = Schema.RequiredMode.REQUIRED) String comment,
32-
@Schema(description = "The display name of the item", requiredMode = Schema.RequiredMode.REQUIRED) String displayName,
33-
@Schema(description = "The material from the item", requiredMode = Schema.RequiredMode.REQUIRED) String material,
34-
@Schema(description = "The group to identify their basic usage", requiredMode = Schema.RequiredMode.REQUIRED) String group,
35-
@Schema(description = "Integer which refers to the customModelData index", requiredMode = Schema.RequiredMode.REQUIRED) int customModelData,
36-
@Schema(description = "The amount of the item", requiredMode = Schema.RequiredMode.REQUIRED) int amount,
35+
@Schema(description = "The display name of the item", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String displayName,
36+
@Schema(description = "The material from the item", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String material,
37+
@Schema(description = "The group to identify their basic usage", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String group,
38+
@Schema(description = "Integer which refers to the customModelData index", requiredMode = Schema.RequiredMode.REQUIRED) @PositiveOrZero int customModelData,
39+
@Schema(description = "The amount of the item", requiredMode = Schema.RequiredMode.REQUIRED) @Positive int amount,
3740
@Schema(description = "The given enchantments", requiredMode = Schema.RequiredMode.NOT_REQUIRED) Map<String, Short> enchantments,
3841
@Schema(description = "The given lore from the item", requiredMode = Schema.RequiredMode.NOT_REQUIRED) List<String> lore,
3942
@Schema(description = "The flags which the item should have", requiredMode = Schema.RequiredMode.NOT_REQUIRED) List<String> flags

src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelDTO.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import io.micronaut.core.annotation.Introspected;
66
import io.micronaut.serde.annotation.Serdeable;
77
import io.swagger.v3.oas.annotations.media.Schema;
8+
import jakarta.validation.constraints.NotBlank;
9+
import jakarta.validation.constraints.NotEmpty;
810
import jakarta.validation.constraints.NotNull;
911
import net.onelitefeather.vulpes.api.model.NotificationEntity;
1012

1113
@Schema(requiredProperties = {
1214
"uiName",
1315
"variableName",
14-
"description",
16+
"comment",
1517
"material",
1618
"frameType",
1719
"title"
@@ -20,24 +22,25 @@
2022
@Serdeable
2123
public record NotificationModelDTO(
2224
@Schema(description = "ID of the notification", requiredMode = Schema.RequiredMode.NOT_REQUIRED) UUID id,
23-
@Schema(description = "Model variableName for the UI", requiredMode = Schema.RequiredMode.REQUIRED) String uiName,
24-
@Schema(description = "Name in the UI", requiredMode = Schema.RequiredMode.REQUIRED) String variableName,
25-
@Schema(description = "Description of the notification", requiredMode = Schema.RequiredMode.REQUIRED) String description,
26-
@Schema(description = "Material identifier", requiredMode = Schema.RequiredMode.REQUIRED) String material,
27-
@Schema(description = "Type of frame", requiredMode = Schema.RequiredMode.REQUIRED) String frameType,
28-
@Schema(description = "Title of the notification", requiredMode = Schema.RequiredMode.REQUIRED) String title
25+
@Schema(description = "Model variableName for the UI", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String uiName,
26+
@Schema(description = "Name in the UI", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String variableName,
27+
@Schema(description = "Comment of the notification", requiredMode = Schema.RequiredMode.REQUIRED) String comment,
28+
@Schema(description = "Material identifier", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String material,
29+
@Schema(description = "Type of frame", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String frameType,
30+
@Schema(description = "Title of the notification", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String title
2931
) {
3032

3133
/**
3234
* Converts this DTO to a {@link NotificationEntity}.
35+
*
3336
* @return a new {@link NotificationEntity} instance with the data from this DTO
3437
*/
3538
public @NotNull NotificationEntity toNotificationModel() {
3639
return new NotificationEntity(
3740
this.id,
3841
uiName,
3942
variableName,
40-
description,
43+
comment,
4144
material,
4245
frameType,
4346
title

src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundEventDTO.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.micronaut.serde.annotation.Serdeable;
55
import io.swagger.v3.oas.annotations.media.Schema;
66
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
7+
import jakarta.validation.constraints.NotBlank;
78
import jakarta.validation.constraints.NotNull;
89
import net.onelitefeather.vulpes.api.model.sound.SoundEventEntity;
910

@@ -37,10 +38,10 @@
3738
@Serdeable
3839
public record SoundEventDTO(
3940
@Schema(description = "Id of the Model", requiredMode = RequiredMode.REQUIRED) UUID id,
40-
@Schema(description = "Name to display it in the ui", requiredMode = RequiredMode.REQUIRED) String uiName,
41-
@Schema(description = "The name which is used for the variable generation", requiredMode = RequiredMode.REQUIRED) String variableName,
42-
@Schema(description = "They key of the sound", requiredMode = RequiredMode.REQUIRED) String keyName,
43-
@Schema(description = "The subtitle which is display when the sound is played", requiredMode = RequiredMode.REQUIRED) String subTitle
41+
@Schema(description = "Name to display it in the ui", requiredMode = RequiredMode.REQUIRED) @NotBlank String uiName,
42+
@Schema(description = "The name which is used for the variable generation", requiredMode = RequiredMode.REQUIRED) @NotBlank String variableName,
43+
@Schema(description = "They key of the sound", requiredMode = RequiredMode.REQUIRED) @NotBlank String keyName,
44+
@Schema(description = "The subtitle which is display when the sound is played", requiredMode = RequiredMode.REQUIRED) @NotBlank String subTitle
4445
) {
4546
/**
4647
* Converts this DTO to a {@link SoundEventEntity}.

src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundFileSourceDTO.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,38 @@
33
import io.micronaut.core.annotation.Introspected;
44
import io.micronaut.serde.annotation.Serdeable;
55
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
67
import jakarta.validation.constraints.NotNull;
8+
import jakarta.validation.constraints.Positive;
9+
import jakarta.validation.constraints.PositiveOrZero;
710
import net.onelitefeather.vulpes.api.model.sound.SoundFileSource;
811

912
import java.util.UUID;
1013

1114
@Schema(
1215
requiredProperties = {
16+
"name",
17+
"volume",
18+
"pitch",
19+
"weight",
20+
"stream",
21+
"attenuationDistance",
22+
"preload",
23+
"type"
1324
}
1425
)
1526
@Introspected
1627
@Serdeable
1728
public record SoundFileSourceDTO(
1829
UUID id,
19-
String name,
20-
float volume,
21-
float pitch,
22-
int weight,
30+
@NotBlank String name,
31+
@PositiveOrZero float volume,
32+
@PositiveOrZero float pitch,
33+
@Positive int weight,
2334
boolean stream,
24-
int attenuationDistance,
35+
@Positive int attenuationDistance,
2536
boolean preload,
26-
String type
37+
@NotBlank String type
2738
) {
2839

2940
/**
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package net.onelitefeather.vulpes.backend.domain;
2+
3+
import jakarta.validation.ConstraintViolation;
4+
import jakarta.validation.Validation;
5+
import jakarta.validation.Validator;
6+
import jakarta.validation.ValidatorFactory;
7+
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
8+
import org.junit.jupiter.api.BeforeAll;
9+
10+
import java.util.Set;
11+
12+
import static org.junit.jupiter.api.Assertions.assertFalse;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
15+
public abstract class ValidationTestBase<T> {
16+
17+
protected static Validator validator;
18+
19+
@BeforeAll
20+
static void setupValidator() {
21+
try (ValidatorFactory factory = Validation.byDefaultProvider()
22+
.configure()
23+
.messageInterpolator(new ParameterMessageInterpolator())
24+
.buildValidatorFactory()) {
25+
validator = factory.getValidator();
26+
}
27+
}
28+
29+
/**
30+
* Helper method to validate a DTO and assert that it has a violation on a specific property
31+
*
32+
* @param dto the DTO to validate
33+
* @param propertyName the name of the property to validate
34+
*/
35+
protected void assertViolation(T dto, String propertyName) {
36+
Set<ConstraintViolation<T>> violations = validator.validate(dto);
37+
assertFalse(violations.isEmpty(), "Expected violations for property: " + propertyName);
38+
boolean found = violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals(propertyName));
39+
if (!found) {
40+
throw new AssertionError("No violation found for property: " + propertyName);
41+
}
42+
}
43+
44+
/**
45+
* Helper method to validate a DTO and assert that it has no violations on a specific property
46+
*
47+
* @param dto the DTO to validate
48+
* @param propertyName the name of the property to validate
49+
*/
50+
protected void assertNoViolation(T dto, String propertyName) {
51+
Set<ConstraintViolation<T>> violations = validator.validate(dto);
52+
assertTrue(violations.isEmpty(), "Expected no violations for property: " + propertyName);
53+
}
54+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package net.onelitefeather.vulpes.backend.domain.attribute.validation;
2+
3+
import net.onelitefeather.vulpes.backend.domain.ValidationTestBase;
4+
import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelDTO;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.UUID;
8+
9+
class AttributeModelDTOValidationTest extends ValidationTestBase<AttributeModelDTO> {
10+
11+
@Test
12+
void testEmptyUiNameValidationFail() {
13+
AttributeModelDTO dto = new AttributeModelDTO(
14+
UUID.randomUUID(),
15+
"",
16+
"empty_value", // invalid
17+
1.0,
18+
5.0
19+
);
20+
21+
assertViolation(dto, "uiName");
22+
}
23+
24+
@Test
25+
void testDefaultValueValidationFail() {
26+
AttributeModelDTO dto = new AttributeModelDTO(
27+
UUID.randomUUID(),
28+
"Speed",
29+
"playerSpeed",
30+
-1.0,
31+
5.0
32+
);
33+
34+
assertViolation(dto, "defaultValue");
35+
}
36+
37+
@Test
38+
void testMaxValueValidationFail() {
39+
AttributeModelDTO dto = new AttributeModelDTO(
40+
UUID.randomUUID(),
41+
"Speed",
42+
"playerSpeed",
43+
0.0,
44+
0.0 // must be strictly positive
45+
);
46+
47+
assertViolation(dto, "maximumValue");
48+
}
49+
50+
@Test
51+
void testEmptyVariableNameValidationFail() {
52+
AttributeModelDTO dto = new AttributeModelDTO(
53+
UUID.randomUUID(),
54+
"Speed",
55+
"",
56+
0.0,
57+
5.0
58+
);
59+
60+
assertViolation(dto, "variableName");
61+
}
62+
}

0 commit comments

Comments
 (0)