This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new b1b73db88 UNOMI-933/934/935: Add REST input validation integration 
tests (#794)
b1b73db88 is described below

commit b1b73db8824343d03fd871f8c6aa9699fa4ee728
Author: Serge Huber <[email protected]>
AuthorDate: Wed Jul 1 13:21:51 2026 +0200

    UNOMI-933/934/935: Add REST input validation integration tests (#794)
    
    Merge additional input validation integration tests
---
 .../test/java/org/apache/unomi/itests/AllITs.java  |   1 +
 .../org/apache/unomi/itests/InputValidationIT.java | 103 +++++++++++++++
 .../unomi/itests/RestCreateValidationIT.java       | 138 +++++++++++++++++++++
 .../contextRequest_eventEmptySource.json           |   9 ++
 .../contextRequest_eventEmptyTarget.json           |   9 ++
 .../validation/rule_invalidCondition.json          |  18 +++
 .../src/test/resources/validation/rule_valid.json  |  21 ++++
 .../validation/segment_invalidCondition.json       |  13 ++
 .../test/resources/validation/segment_valid.json   |  12 ++
 9 files changed, 324 insertions(+)

diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java 
b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index ace015dfb..3ea7dd340 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -51,6 +51,7 @@ import org.junit.runners.Suite.SuiteClasses;
         CopyPropertiesActionIT.class,
         IncrementPropertyIT.class,
         InputValidationIT.class,
+        RestCreateValidationIT.class,
         ModifyConsentIT.class,
         PatchIT.class,
         ContextServletIT.class,
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java 
b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
index 316ca5354..dd9ac69a7 100644
--- a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
@@ -16,6 +16,7 @@
  */
 package org.apache.unomi.itests;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
@@ -42,6 +43,7 @@ import java.util.Map;
 import java.util.Objects;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
 @RunWith(PaxExam.class)
@@ -235,6 +237,107 @@ public class InputValidationIT extends BaseIT {
         doGETRequestTest(CONTEXT_JS_URL, headers, null, 200, null);
     }
 
+
+    /**
+     * UNOMI-933: unparseable context request bodies on {@code /context.json} 
and {@code /context.js}
+     * must return 400, not 500. Deserialization failures return JSON {@code 
{"errorMessage":"badRequest"}};
+     * schema-level validation returns plain text.
+     */
+    @Test
+    public void test_contextRequest_garbageBody() throws Exception {
+        // test JSON endpoint (and its /cxs/ mirror via 
doPOSTRawBodyTestBadRequest)
+        doPOSTRawBodyTestBadRequest(CONTEXT_JSON_URL + 
"?sessionId=dummy-session-id", "foo");
+        // test JS endpoint (and its /cxs/ mirror)
+        doPOSTRawBodyTestBadRequest(CONTEXT_JS_URL + 
"?sessionId=dummy-session-id", "foo");
+    }
+
+    @Test
+    public void test_contextRequest_eventEmptySource() throws Exception {
+        doPOSTRequestTestBadRequest(CONTEXT_JSON_URL, null, 
"/validation/contextRequest_eventEmptySource.json");
+        doGETRequestTestBadRequest(CONTEXT_JSON_URL, 
"/validation/contextRequest_eventEmptySource.json");
+    }
+
+    @Test
+    public void test_contextRequest_eventEmptyTarget() throws Exception {
+        doPOSTRequestTestBadRequest(CONTEXT_JSON_URL, null, 
"/validation/contextRequest_eventEmptyTarget.json");
+        doGETRequestTestBadRequest(CONTEXT_JSON_URL, 
"/validation/contextRequest_eventEmptyTarget.json");
+    }
+
+    private void doPOSTRawBodyTestBadRequest(String uri, String rawBody) 
throws Exception {
+        // test old servlets
+        performPOSTRawBodyTestBadRequest(getFullUrl(uri), rawBody);
+        // test directly CXS endpoints
+        performPOSTRawBodyTestBadRequest(getFullUrl("/cxs" + uri), rawBody);
+    }
+
+    private void doGETRequestTestBadRequest(String uri, String 
entityResourcePath) throws Exception {
+        // test old servlets
+        performGETRequestTestBadRequest(getFullUrl(uri), entityResourcePath);
+        // test directly CXS endpoints
+        performGETRequestTestBadRequest(getFullUrl("/cxs" + uri), 
entityResourcePath);
+    }
+
+    private void performGETRequestTestBadRequest(String url, String 
entityResourcePath) throws Exception {
+        if (entityResourcePath != null) {
+            String payload = getValidatedBundleJSON(entityResourcePath, new 
HashMap<>());
+            url += (url.contains("?") ? "&" : "?") + "payload=" + 
URLEncoder.encode(payload, StandardCharsets.UTF_8.toString());
+        }
+        performRequestExpectBadRequest(new HttpGet(url), null);
+    }
+
+    private void doPOSTRequestTestBadRequest(String uri, Map<String, String> 
headers, String entityResourcePath)
+            throws Exception {
+        performPOSTRequestTestBadRequest(getFullUrl(uri), headers, 
entityResourcePath);
+        performPOSTRequestTestBadRequest(getFullUrl("/cxs" + uri), headers, 
entityResourcePath);
+    }
+
+    private void performPOSTRawBodyTestBadRequest(String url, String rawBody) 
throws IOException {
+        HttpPost request = new HttpPost(url);
+        request.setHeader("Content-Type", "application/json");
+        request.setEntity(new StringEntity(rawBody, 
ContentType.APPLICATION_JSON));
+        performRequestExpectBadRequest(request, null);
+    }
+
+    private void performPOSTRequestTestBadRequest(String url, Map<String, 
String> headers, String entityResourcePath)
+            throws IOException {
+        HttpPost request = new HttpPost(url);
+        if (headers == null) {
+            headers = new HashMap<>();
+        }
+        headers.put("Content-Type", "application/json");
+        if (entityResourcePath != null) {
+            request.setEntity(new 
StringEntity(getValidatedBundleJSON(entityResourcePath, new HashMap<>()), 
ContentType.create("application/json")));
+        }
+        performRequestExpectBadRequest(request, headers);
+    }
+
+    private void performRequestExpectBadRequest(HttpUriRequest request, 
Map<String, String> headers) throws IOException {
+        CloseableHttpResponse response;
+        if (headers != null && !headers.isEmpty()) {
+            for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
+                request.addHeader(headerEntry.getKey(), 
headerEntry.getValue());
+            }
+        }
+        try {
+            response = HttpClientThatWaitsForUnomi.doRequest(request, 400);
+        } catch (Exception e) {
+            fail("Something went wrong with the request to Unomi that is 
unexpected: " + e.getMessage());
+            return;
+        }
+        assertEquals("Invalid response code", 400, 
response.getStatusLine().getStatusCode());
+        
assertInvalidClientDataResponse(EntityUtils.toString(response.getEntity()));
+    }
+
+    private void assertInvalidClientDataResponse(String responseBody) throws 
IOException {
+        if (ERROR_MESSAGE_INVALID_DATA_RECEIVED.equals(responseBody)) {
+            return;
+        }
+        JsonNode json = objectMapper.readTree(responseBody);
+        JsonNode errorNode = json.get("errorMessage");
+        assertNotNull("Response JSON missing 'errorMessage' field. Body: " + 
responseBody, errorNode);
+        assertEquals("badRequest", errorNode.asText());
+    }
+
     private void doGETRequestTest(String uri, Map<String, String> headers, 
String entityResourcePath, int expectedHTTPStatusCode, String 
expectedErrorMessage) throws Exception {
         // test old servlets
         performGETRequestTest(getFullUrl(uri), headers, entityResourcePath, 
expectedHTTPStatusCode, expectedErrorMessage);
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java 
b/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java
new file mode 100644
index 000000000..cff021b3a
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Segment;
+import org.apache.unomi.itests.tools.LogChecker;
+import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Integration tests for REST create endpoints returning HTTP 400 on invalid 
payloads
+ * (UNOMI-934 rules, UNOMI-935 segments). Verifies both the HTTP status code 
and the
+ * JSON response contract: {@code {"errorMessage":"badRequest"}}.
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class RestCreateValidationIT extends BaseIT {
+
+    private static final String RULES_URL = "/cxs/rules";
+    private static final String SEGMENTS_URL = "/cxs/segments";
+
+    @Override
+    protected LogChecker createLogChecker() {
+        return LogChecker.builder()
+                .addIgnoredSubstring("Response status code: 400")
+                .addIgnoredSubstring("Invalid rule condition")
+                .addIgnoredSubstring("BadSegmentConditionException")
+                .addIgnoredSubstring("IllegalArgumentExceptionMapper")
+                .addIgnoredSubstring("BadSegmentConditionExceptionMapper")
+                .addIgnoredSubstring("JsonMappingExceptionMapper")
+                // RuntimeExceptionMapper logs "Bad request on ..." at WARN 
for client errors
+                .addIgnoredSubstring("Bad request on")
+                .build();
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        removeItems(Rule.class, Segment.class);
+    }
+
+    @Test
+    public void testCreateValidRule_returns204() throws Exception {
+        doPostRawBodyTest(RULES_URL, 
getValidatedBundleJSON("/validation/rule_valid.json", new HashMap<>()), 204);
+    }
+
+    @Test
+    public void testCreateValidSegment_returns204() throws Exception {
+        doPostRawBodyTest(SEGMENTS_URL, 
getValidatedBundleJSON("/validation/segment_valid.json", new HashMap<>()), 204);
+    }
+
+    @Test
+    public void testCreateRuleWithInvalidCondition_returnsBadRequest() throws 
Exception {
+        doPostRawBodyTest(RULES_URL, 
getValidatedBundleJSON("/validation/rule_invalidCondition.json", new 
HashMap<>()), 400);
+    }
+
+    @Test
+    public void testCreateRuleWithMalformedJson_returnsBadRequest() throws 
Exception {
+        doPostRawBodyTest(RULES_URL, "foo", 400);
+    }
+
+    @Test
+    public void testCreateSegmentWithInvalidCondition_returnsBadRequest() 
throws Exception {
+        doPostRawBodyTest(SEGMENTS_URL, 
getValidatedBundleJSON("/validation/segment_invalidCondition.json", new 
HashMap<>()), 400);
+    }
+
+    @Test
+    public void testCreateSegmentWithMalformedJson_returnsBadRequest() throws 
Exception {
+        doPostRawBodyTest(SEGMENTS_URL, "foo", 400);
+    }
+
+    private void doPostRawBodyTest(String uri, String body, int 
expectedStatusCode) throws Exception {
+        performRestPostTest(getFullUrl(uri), body, expectedStatusCode);
+    }
+
+    private void performRestPostTest(String url, String body, int 
expectedStatusCode) throws IOException {
+        HttpPost request = new HttpPost(url);
+        request.setHeader("Content-Type", "application/json");
+        request.setEntity(new StringEntity(body, 
ContentType.APPLICATION_JSON));
+        CloseableHttpResponse response;
+        try {
+            response = HttpClientThatWaitsForUnomi.doRequest(request, 
expectedStatusCode);
+        } catch (Exception e) {
+            fail("Request to " + url + " failed unexpectedly: " + 
e.getMessage());
+            return;
+        }
+        assertEquals("Unexpected HTTP status for POST " + url, 
expectedStatusCode, response.getStatusLine().getStatusCode());
+        if (expectedStatusCode == 400) {
+            assertBadRequestResponse(url, 
EntityUtils.toString(response.getEntity()));
+        }
+    }
+
+    private void assertBadRequestResponse(String url, String responseBody) 
throws IOException {
+        assertNotNull("Expected a response body for 400 on POST " + url, 
responseBody);
+        JsonNode json;
+        try {
+            json = objectMapper.readTree(responseBody);
+        } catch (Exception e) {
+            fail("Expected JSON in 400 response for POST " + url + " but got: 
" + responseBody);
+            return;
+        }
+        JsonNode errorNode = json.get("errorMessage");
+        assertNotNull("Response JSON missing 'errorMessage' field for POST " + 
url + ". Body: " + responseBody, errorNode);
+        assertEquals("badRequest", errorNode.asText());
+    }
+}
diff --git 
a/itests/src/test/resources/validation/contextRequest_eventEmptySource.json 
b/itests/src/test/resources/validation/contextRequest_eventEmptySource.json
new file mode 100644
index 000000000..725762e64
--- /dev/null
+++ b/itests/src/test/resources/validation/contextRequest_eventEmptySource.json
@@ -0,0 +1,9 @@
+{
+  "sessionId": "dummy-session-id",
+  "events": [
+    {
+      "eventType": "view",
+      "source": {}
+    }
+  ]
+}
diff --git 
a/itests/src/test/resources/validation/contextRequest_eventEmptyTarget.json 
b/itests/src/test/resources/validation/contextRequest_eventEmptyTarget.json
new file mode 100644
index 000000000..1e1ff7541
--- /dev/null
+++ b/itests/src/test/resources/validation/contextRequest_eventEmptyTarget.json
@@ -0,0 +1,9 @@
+{
+  "sessionId": "dummy-session-id",
+  "events": [
+    {
+      "eventType": "view",
+      "target": {}
+    }
+  ]
+}
diff --git a/itests/src/test/resources/validation/rule_invalidCondition.json 
b/itests/src/test/resources/validation/rule_invalidCondition.json
new file mode 100644
index 000000000..e5bf459ba
--- /dev/null
+++ b/itests/src/test/resources/validation/rule_invalidCondition.json
@@ -0,0 +1,18 @@
+{
+  "metadata": {
+    "id": "it-validation-invalid-rule",
+    "name": "Invalid rule IT",
+    "scope": "systemscope",
+    "enabled": true
+  },
+  "condition": {},
+  "actions": [
+    {
+      "type": "setPropertyAction",
+      "parameterValues": {
+        "setPropertyName": "properties.itValidation",
+        "setPropertyValue": "test"
+      }
+    }
+  ]
+}
diff --git a/itests/src/test/resources/validation/rule_valid.json 
b/itests/src/test/resources/validation/rule_valid.json
new file mode 100644
index 000000000..02e5b12c8
--- /dev/null
+++ b/itests/src/test/resources/validation/rule_valid.json
@@ -0,0 +1,21 @@
+{
+  "metadata": {
+    "id": "it-validation-valid-rule",
+    "name": "Valid rule IT",
+    "scope": "systemscope",
+    "enabled": false
+  },
+  "condition": {
+    "type": "matchAllCondition",
+    "parameterValues": {}
+  },
+  "actions": [
+    {
+      "type": "setPropertyAction",
+      "parameterValues": {
+        "setPropertyName": "properties.itValidation",
+        "setPropertyValue": "test"
+      }
+    }
+  ]
+}
diff --git a/itests/src/test/resources/validation/segment_invalidCondition.json 
b/itests/src/test/resources/validation/segment_invalidCondition.json
new file mode 100644
index 000000000..d184f0fe8
--- /dev/null
+++ b/itests/src/test/resources/validation/segment_invalidCondition.json
@@ -0,0 +1,13 @@
+{
+  "metadata": {
+    "id": "it-validation-invalid-segment",
+    "name": "Invalid segment IT",
+    "scope": "systemscope"
+  },
+  "condition": {
+    "type": "fakeConditionId",
+    "parameterValues": {
+      "param": "param value"
+    }
+  }
+}
diff --git a/itests/src/test/resources/validation/segment_valid.json 
b/itests/src/test/resources/validation/segment_valid.json
new file mode 100644
index 000000000..a02f49f63
--- /dev/null
+++ b/itests/src/test/resources/validation/segment_valid.json
@@ -0,0 +1,12 @@
+{
+  "metadata": {
+    "id": "it-validation-valid-segment",
+    "name": "Valid segment IT",
+    "scope": "systemscope",
+    "enabled": false
+  },
+  "condition": {
+    "type": "matchAllCondition",
+    "parameterValues": {}
+  }
+}

Reply via email to