diff --git a/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java b/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java index ffe1dde5d9..b8d140e70b 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java @@ -7,7 +7,25 @@ public class ValidationUtils { - public static final String NULL_OR_NOT_BLANK = "^(?!\\s*$).+"; + /** + * Regular expression to validate if a string is null or not blank. + * + *

It matches any string that is not null and contains at least one non-whitespace character.

+ * For example: + * + * + * @see Visual Explainer + * @see Ai Explainer + */ + public static final String NULL_OR_NOT_BLANK = "(?s)^\\s*(\\S.*\\S|\\S)\\s*$"; /** * Canonical String representation to ensure precision over float or double. diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java index 9b6a410847..d6c7df2fa8 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java @@ -1197,6 +1197,22 @@ void create__success() { createAndAssert(dataset); } + @Test + @DisplayName("when description is multiline, then accept the request") + void create__whenDescriptionIsMultiline__thenAcceptTheRequest() { + + var dataset = factory.manufacturePojo(Dataset.class).toBuilder() + .id(null) + .description(""" + Test + Description + """ + ) + .build(); + + createAndAssert(dataset); + } + @Test @DisplayName("when creating datasets with same name in different workspaces, then accept the request") void create__whenCreatingDatasetsWithSameNameInDifferentWorkspaces__thenAcceptTheRequest() { diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java index ea8b879d8a..193909b6ef 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java @@ -854,6 +854,30 @@ void create__whenWorkspaceNameIsSpecified__thenAcceptTheRequest() { assertProject(project.toBuilder().id(id).build(), apiKey, workspaceName); } + @Test + @DisplayName("when workspace description is multiline, then accept the request") + void create__whenDescriptionIsMultiline__thenAcceptTheRequest() { + var project = factory.manufacturePojo(Project.class); + + project = project.toBuilder().description("Test Project\n\nMultiline Description").build(); + + UUID id; + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)).request() + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(HttpHeaders.AUTHORIZATION, API_KEY) + .header(WORKSPACE_HEADER, TEST_WORKSPACE) + .post(Entity.json(project))) { + + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); + assertThat(actualResponse.hasEntity()).isFalse(); + assertThat(actualResponse.getHeaderString("Location")).matches(Pattern.compile(URL_PATTERN)); + + id = TestUtils.getIdFromLocation(actualResponse.getLocation()); + } + + assertProject(project.toBuilder().id(id).build()); + } + @Test @DisplayName("when description is null, then accept the request") void create__whenDescriptionIsNull__thenAcceptNameCreate() { diff --git a/apps/opik-backend/src/test/java/com/comet/opik/utils/ValidationUtilsTest.java b/apps/opik-backend/src/test/java/com/comet/opik/utils/ValidationUtilsTest.java new file mode 100644 index 0000000000..f0b2f978c8 --- /dev/null +++ b/apps/opik-backend/src/test/java/com/comet/opik/utils/ValidationUtilsTest.java @@ -0,0 +1,30 @@ +package com.comet.opik.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ValidationUtilsTest { + + public static Stream testNullOrNotBlank() { + return Stream.of( + Arguments.of("", false), + Arguments.of(" ", false), + Arguments.of("\n", false), + Arguments.of("a", true), + Arguments.of(" a ", true), + Arguments.of("\n a \n", true) + ); + } + + @ParameterizedTest + @MethodSource + void testNullOrNotBlank(String input, boolean expected) { + assertEquals(expected, input.matches(ValidationUtils.NULL_OR_NOT_BLANK)); + } + +} \ No newline at end of file