You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2026/07/13 11:52:19 UTC
(unomi) branch fix/UNOMI-962-rest-api-hardening created (now 49e4c70a6)
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a change to branch fix/UNOMI-962-rest-api-hardening
in repository https://gitbox.apache.org/repos/asf/unomi.git
at 49e4c70a6 UNOMI-962: Harden REST API for missing resources, exports, and OpenSearch queries
This branch includes the following new commits:
new 49e4c70a6 UNOMI-962: Harden REST API for missing resources, exports, and OpenSearch queries
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(unomi) 01/01: UNOMI-962: Harden REST API for missing resources, exports, and OpenSearch queries
Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch fix/UNOMI-962-rest-api-hardening
in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 49e4c70a67138ba69c22de0bbb9b99442307d947
Author: Serge Huber <sh...@apache.org>
AuthorDate: Mon Jul 13 13:51:55 2026 +0200
UNOMI-962: Harden REST API for missing resources, exports, and OpenSearch queries
Return 404 for missing value types, goals, goal reports, and events instead
of server errors. Validate router export configuration, guard goal reports and
profile CSV export against missing metadata, align OpenSearch null property
values with Elasticsearch, inject UserListService contextManager, and add unit
tests for each fix area.
---
extensions/router/router-service/pom.xml | 15 ++
.../router/services/ProfileExportServiceImpl.java | 12 ++
.../services/ProfileExportServiceImplTest.java | 105 ++++++++++++++
.../ElasticSearchPersistenceServiceImpl.java | 2 +-
persistence-opensearch/core/pom.xml | 15 ++
.../OpenSearchPersistenceServiceImpl.java | 2 +-
.../core/PropertyConditionOSQueryBuilder.java | 2 +
.../core/PropertyConditionOSQueryBuilderTest.java | 68 +++++++++
.../rest/endpoints/DefinitionsServiceEndPoint.java | 3 +
.../unomi/rest/endpoints/EventServiceEndpoint.java | 9 +-
.../unomi/rest/endpoints/GoalsServiceEndPoint.java | 6 +
.../endpoints/MissingResourceEndpointsTest.java | 158 +++++++++++++++++++++
.../services/impl/goals/GoalsServiceImpl.java | 3 +
.../services/impl/profiles/ProfileServiceImpl.java | 6 +-
.../resources/OSGI-INF/blueprint/blueprint.xml | 1 +
.../services/impl/goals/GoalsServiceImplTest.java | 7 +
.../impl/profiles/ProfileServiceImplTest.java | 29 +++-
17 files changed, 437 insertions(+), 6 deletions(-)
diff --git a/extensions/router/router-service/pom.xml b/extensions/router/router-service/pom.xml
index 15d9629b4..19393d9bb 100644
--- a/extensions/router/router-service/pom.xml
+++ b/extensions/router/router-service/pom.xml
@@ -82,6 +82,21 @@
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java
index 7a1fc34ec..aea02cf38 100644
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java
+++ b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java
@@ -59,6 +59,15 @@ public class ProfileExportServiceImpl implements ProfileExportService {
}
public String extractProfilesBySegment(ExportConfiguration exportConfiguration) {
+ Object segmentProperty = exportConfiguration.getProperty("segment");
+ if (segmentProperty == null || StringUtils.isBlank(segmentProperty.toString())) {
+ throw new IllegalArgumentException("Export segment is required");
+ }
+ Map<String, String> mapping = (Map<String, String>) exportConfiguration.getProperty("mapping");
+ if (mapping == null || mapping.isEmpty()) {
+ throw new IllegalArgumentException("Export mapping is required");
+ }
+
Collection<PropertyType> propertiesDef = persistenceService.query("target", "profiles", null, PropertyType.class);
Condition segmentCondition = new Condition();
@@ -97,6 +106,9 @@ public class ProfileExportServiceImpl implements ProfileExportService {
public String convertProfileToCSVLine(Profile profile, ExportConfiguration exportConfiguration, Collection<PropertyType> propertiesDef) {
Map<String, String> mapping = (Map<String, String>) exportConfiguration.getProperty("mapping");
+ if (mapping == null || mapping.isEmpty()) {
+ throw new IllegalArgumentException("Export mapping is required");
+ }
String lineToWrite = "";
for (int i = 0; i < mapping.size(); i++) {
String propertyName = mapping.get(String.valueOf(i));
diff --git a/extensions/router/router-service/src/test/java/org/apache/unomi/router/services/ProfileExportServiceImplTest.java b/extensions/router/router-service/src/test/java/org/apache/unomi/router/services/ProfileExportServiceImplTest.java
new file mode 100644
index 000000000..d3efb6016
--- /dev/null
+++ b/extensions/router/router-service/src/test/java/org/apache/unomi/router/services/ProfileExportServiceImplTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.router.services;
+
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.router.api.ExportConfiguration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ProfileExportServiceImplTest {
+
+ private ProfileExportServiceImpl profileExportService;
+
+ @BeforeEach
+ void setUp() {
+ profileExportService = new ProfileExportServiceImpl();
+ }
+
+ @Test
+ void extractProfilesBySegment_missingSegment_throwsIllegalArgumentException() {
+ ExportConfiguration configuration = new ExportConfiguration();
+ configuration.setProperty("mapping", Map.of("0", "firstName"));
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> profileExportService.extractProfilesBySegment(configuration));
+
+ assertEquals("Export segment is required", exception.getMessage());
+ }
+
+ @Test
+ void extractProfilesBySegment_blankSegment_throwsIllegalArgumentException() {
+ ExportConfiguration configuration = new ExportConfiguration();
+ configuration.setProperty("segment", " ");
+ configuration.setProperty("mapping", Map.of("0", "firstName"));
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> profileExportService.extractProfilesBySegment(configuration));
+
+ assertEquals("Export segment is required", exception.getMessage());
+ }
+
+ @Test
+ void extractProfilesBySegment_missingMapping_throwsIllegalArgumentException() {
+ ExportConfiguration configuration = new ExportConfiguration();
+ configuration.setProperty("segment", "frequent-buyers");
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> profileExportService.extractProfilesBySegment(configuration));
+
+ assertEquals("Export mapping is required", exception.getMessage());
+ }
+
+ @Test
+ void convertProfileToCSVLine_missingMapping_throwsIllegalArgumentException() {
+ ExportConfiguration configuration = new ExportConfiguration();
+ Profile profile = new Profile("profile-1");
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> profileExportService.convertProfileToCSVLine(profile, configuration, Collections.emptyList()));
+
+ assertEquals("Export mapping is required", exception.getMessage());
+ }
+
+ @Test
+ void convertProfileToCSVLine_nullPropertyValue_writesEmptyField() {
+ ExportConfiguration configuration = new ExportConfiguration();
+ configuration.setColumnSeparator(",");
+ Map<String, String> mapping = new HashMap<>();
+ mapping.put("0", "firstName");
+ configuration.setProperty("mapping", mapping);
+
+ Profile profile = new Profile("profile-1");
+ PropertyType propertyType = new PropertyType();
+ propertyType.setMetadata(new Metadata("firstName"));
+
+ String line = profileExportService.convertProfileToCSVLine(profile, configuration,
+ Collections.singletonList(propertyType));
+
+ assertTrue(line.isEmpty());
+ }
+}
diff --git a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
index d900c6975..2756e78ad 100644
--- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
+++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
@@ -1754,7 +1754,7 @@ public class ElasticSearchPersistenceServiceImpl implements PersistenceService,
Map<String, Object> subSubMappings = (Map<String, Object>) subMappings.computeIfAbsent("properties", k -> new HashMap<>());
if (subSubMappings.containsKey(property.getItemId())) {
- LOGGER.warn("Mapping already exists for type {} and property {}", itemType, property.getItemId());
+ LOGGER.debug("Mapping already exists for type {} and property {}", itemType, property.getItemId());
return;
}
diff --git a/persistence-opensearch/core/pom.xml b/persistence-opensearch/core/pom.xml
index 90adbc735..023565513 100644
--- a/persistence-opensearch/core/pom.xml
+++ b/persistence-opensearch/core/pom.xml
@@ -136,6 +136,21 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
diff --git a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
index 0888dbc36..7451ee5a5 100644
--- a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
+++ b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
@@ -1711,7 +1711,7 @@ public class OpenSearchPersistenceServiceImpl implements PersistenceService, Syn
Map<String, Object> subSubMappings = (Map<String, Object>) subMappings.computeIfAbsent("properties", k -> new HashMap<>());
if (subSubMappings.containsKey(property.getItemId())) {
- LOGGER.warn("Mapping already exists for type " + itemType + " and property " + property.getItemId());
+ LOGGER.debug("Mapping already exists for type " + itemType + " and property " + property.getItemId());
return;
}
diff --git a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilder.java b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilder.java
index de7547b88..22787c1b5 100644
--- a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilder.java
+++ b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilder.java
@@ -241,6 +241,8 @@ public class PropertyConditionOSQueryBuilder implements ConditionOSQueryBuilder
return fieldValueBuilder.stringValue(convertDateToISO((Date) fieldValue).toString());
} else if (fieldValue instanceof OffsetDateTime) {
return fieldValueBuilder.stringValue(convertDateToISO((OffsetDateTime) fieldValue).toString());
+ } else if (fieldValue == null) {
+ throw new IllegalArgumentException("Impossible to build OS filter, unsupported value type: null");
} else {
throw new IllegalArgumentException("Impossible to build OS filter, unsupported value type: " + fieldValue.getClass().getName());
}
diff --git a/persistence-opensearch/core/src/test/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilderTest.java b/persistence-opensearch/core/src/test/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilderTest.java
new file mode 100644
index 000000000..fe4a0f72f
--- /dev/null
+++ b/persistence-opensearch/core/src/test/java/org/apache/unomi/persistence/opensearch/querybuilders/core/PropertyConditionOSQueryBuilderTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.persistence.opensearch.querybuilders.core;
+
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.persistence.opensearch.ConditionOSQueryBuilderDispatcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(MockitoExtension.class)
+class PropertyConditionOSQueryBuilderTest {
+
+ @Mock
+ private ConditionOSQueryBuilderDispatcher dispatcher;
+
+ @Test
+ void buildQuery_inOperatorWithNullValue_throwsIllegalArgumentException() {
+ Condition condition = new Condition();
+ condition.setParameter("comparisonOperator", "in");
+ condition.setParameter("propertyName", "properties.firstName");
+ condition.setParameter("propertyValues", Arrays.asList("Jane", null));
+
+ PropertyConditionOSQueryBuilder builder = new PropertyConditionOSQueryBuilder();
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> builder.buildQuery(condition, Collections.emptyMap(), dispatcher));
+
+ assertTrue(exception.getMessage().contains("null"));
+ }
+
+ @Test
+ void buildQuery_equalsWithMissingValue_throwsIllegalArgumentException() {
+ Condition condition = new Condition();
+ condition.setParameter("comparisonOperator", "equals");
+ condition.setParameter("propertyName", "properties.firstName");
+
+ PropertyConditionOSQueryBuilder builder = new PropertyConditionOSQueryBuilder();
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> builder.buildQuery(condition, Collections.emptyMap(), dispatcher));
+
+ assertEquals("Impossible to build OS filter, missing value for condition using comparisonOperator: equals, and propertyName: properties.firstName",
+ exception.getMessage());
+ }
+}
diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/DefinitionsServiceEndPoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/DefinitionsServiceEndPoint.java
index 97b9a1697..2a2c3c62d 100644
--- a/rest/src/main/java/org/apache/unomi/rest/endpoints/DefinitionsServiceEndPoint.java
+++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/DefinitionsServiceEndPoint.java
@@ -281,6 +281,9 @@ public class DefinitionsServiceEndPoint {
@Path("/values/{valueTypeId}")
public RESTValueType getValueType(@PathParam("valueTypeId") String id, @HeaderParam("Accept-Language") String language) {
ValueType valueType = definitionsService.getValueType(id);
+ if (valueType == null) {
+ throw new NotFoundException("Value type not found: " + id);
+ }
return localizationHelper.generateValueType(valueType, language);
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java
index c36ff131a..e3497922d 100644
--- a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java
+++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java
@@ -26,6 +26,7 @@ import org.osgi.service.component.annotations.Reference;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
import java.util.Set;
/**
@@ -72,8 +73,12 @@ public class EventServiceEndpoint {
*/
@GET
@Path("/{id}")
- public Event getEvents(@PathParam("id") final String id) {
- return eventService.getEvent(id);
+ public Response getEvents(@PathParam("id") final String id) {
+ Event event = eventService.getEvent(id);
+ if (event == null) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ return Response.ok(event).build();
}
/**
diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/GoalsServiceEndPoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/GoalsServiceEndPoint.java
index 3c750fd69..2236ba876 100644
--- a/rest/src/main/java/org/apache/unomi/rest/endpoints/GoalsServiceEndPoint.java
+++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/GoalsServiceEndPoint.java
@@ -123,6 +123,9 @@ public class GoalsServiceEndPoint {
@GET
@Path("/{goalID}/report")
public GoalReport getGoalReport(@PathParam("goalID") String goalId) {
+ if (goalsService.getGoal(goalId) == null) {
+ throw new NotFoundException("Goal not found: " + goalId);
+ }
return goalsService.getGoalReport(goalId);
}
@@ -136,6 +139,9 @@ public class GoalsServiceEndPoint {
@POST
@Path("/{goalID}/report")
public GoalReport getGoalReport(@PathParam("goalID") String goalId, AggregateQuery query) {
+ if (goalsService.getGoal(goalId) == null) {
+ throw new NotFoundException("Goal not found: " + goalId);
+ }
return goalsService.getGoalReport(goalId, query);
}
}
diff --git a/rest/src/test/java/org/apache/unomi/rest/endpoints/MissingResourceEndpointsTest.java b/rest/src/test/java/org/apache/unomi/rest/endpoints/MissingResourceEndpointsTest.java
new file mode 100644
index 000000000..e3199b6be
--- /dev/null
+++ b/rest/src/test/java/org/apache/unomi/rest/endpoints/MissingResourceEndpointsTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.rest.endpoints;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.ValueType;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.goals.GoalReport;
+import org.apache.unomi.api.query.AggregateQuery;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.rest.models.RESTValueType;
+import org.apache.unomi.rest.service.impl.LocalizationHelper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MissingResourceEndpointsTest {
+
+ @Mock
+ private DefinitionsService definitionsService;
+
+ @Mock
+ private LocalizationHelper localizationHelper;
+
+ @Mock
+ private GoalsService goalsService;
+
+ @Mock
+ private EventService eventService;
+
+ @Test
+ void getValueType_missingValueType_throwsNotFound() {
+ when(definitionsService.getValueType("missing-type")).thenReturn(null);
+
+ DefinitionsServiceEndPoint endpoint = new DefinitionsServiceEndPoint();
+ endpoint.setDefinitionsService(definitionsService);
+ endpoint.setLocalizationHelper(localizationHelper);
+
+ NotFoundException exception = assertThrows(NotFoundException.class,
+ () -> endpoint.getValueType("missing-type", "en"));
+
+ assertTrue(exception.getMessage().contains("missing-type"));
+ verify(localizationHelper, never()).generateValueType(any(ValueType.class), anyString());
+ }
+
+ @Test
+ void getValueType_existingValueType_returnsLocalizedValueType() {
+ ValueType valueType = new ValueType();
+ valueType.setId("string");
+ RESTValueType restValueType = new RESTValueType();
+ restValueType.setId("string");
+
+ when(definitionsService.getValueType("string")).thenReturn(valueType);
+ when(localizationHelper.generateValueType(valueType, "en")).thenReturn(restValueType);
+
+ DefinitionsServiceEndPoint endpoint = new DefinitionsServiceEndPoint();
+ endpoint.setDefinitionsService(definitionsService);
+ endpoint.setLocalizationHelper(localizationHelper);
+
+ RESTValueType result = endpoint.getValueType("string", "en");
+
+ assertEquals("string", result.getId());
+ }
+
+ @Test
+ void getGoalReport_missingGoal_throwsNotFound() {
+ when(goalsService.getGoal("missing-goal")).thenReturn(null);
+
+ GoalsServiceEndPoint endpoint = new GoalsServiceEndPoint();
+ endpoint.setGoalsService(goalsService);
+
+ assertThrows(NotFoundException.class, () -> endpoint.getGoalReport("missing-goal"));
+ }
+
+ @Test
+ void getGoalReportPost_missingGoal_throwsNotFound() {
+ when(goalsService.getGoal("missing-goal")).thenReturn(null);
+
+ GoalsServiceEndPoint endpoint = new GoalsServiceEndPoint();
+ endpoint.setGoalsService(goalsService);
+
+ assertThrows(NotFoundException.class,
+ () -> endpoint.getGoalReport("missing-goal", new AggregateQuery()));
+ }
+
+ @Test
+ void getGoalReport_existingGoal_delegatesToService() {
+ Goal goal = new Goal();
+ goal.setItemId("goal-1");
+ GoalReport report = new GoalReport();
+
+ when(goalsService.getGoal("goal-1")).thenReturn(goal);
+ when(goalsService.getGoalReport("goal-1")).thenReturn(report);
+
+ GoalsServiceEndPoint endpoint = new GoalsServiceEndPoint();
+ endpoint.setGoalsService(goalsService);
+
+ assertEquals(report, endpoint.getGoalReport("goal-1"));
+ }
+
+ @Test
+ void getEvent_missingEvent_returnsNotFound() {
+ when(eventService.getEvent("missing-event")).thenReturn(null);
+
+ EventServiceEndpoint endpoint = new EventServiceEndpoint();
+ endpoint.setEventService(eventService);
+
+ Response response = endpoint.getEvents("missing-event");
+
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ void getEvent_existingEvent_returnsOkWithBody() {
+ Event event = new Event();
+ event.setItemId("event-1");
+
+ when(eventService.getEvent("event-1")).thenReturn(event);
+
+ EventServiceEndpoint endpoint = new EventServiceEndpoint();
+ endpoint.setEventService(eventService);
+
+ Response response = endpoint.getEvents("event-1");
+
+ assertEquals(200, response.getStatus());
+ assertEquals(event, response.getEntity());
+ }
+}
diff --git a/services/src/main/java/org/apache/unomi/services/impl/goals/GoalsServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/goals/GoalsServiceImpl.java
index af4724686..18f7b2637 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/goals/GoalsServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/goals/GoalsServiceImpl.java
@@ -518,6 +518,9 @@ public class GoalsServiceImpl extends AbstractMultiTypeCachingService implements
condition.setParameter("subConditions", list);
Goal g = getGoal(goalId);
+ if (g == null) {
+ return null;
+ }
Condition goalTargetCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
goalTargetCondition.setParameter("propertyName", "systemProperties.goals." + goalId+ "TargetReached");
diff --git a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
index 352849cb8..f24636ca9 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
@@ -541,7 +541,11 @@ public class ProfileServiceImpl extends AbstractMultiTypeCachingService implemen
List<String> segmentNames = new ArrayList<String>();
for (String segment : profile.getSegments()) {
Segment s = segmentService.getSegmentDefinition(segment);
- segmentNames.add(csvEncode(s.getMetadata().getName()));
+ if (s == null || s.getMetadata() == null) {
+ segmentNames.add(csvEncode(segment));
+ } else {
+ segmentNames.add(csvEncode(s.getMetadata().getName()));
+ }
}
sb.append(csvEncode(StringUtils.join(segmentNames, ",")));
sb.append('\n');
diff --git a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index b83b0daf8..6002b721a 100644
--- a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -378,6 +378,7 @@
<property name="persistenceService" ref="persistenceService"/>
<property name="definitionsService" ref="definitionsServiceImpl"/>
<property name="bundleContext" ref="blueprintBundleContext"/>
+ <property name="contextManager" ref="executionContextManager"/>
</bean>
<bean id="profileServiceImpl" class="org.apache.unomi.services.impl.profiles.ProfileServiceImpl"
diff --git a/services/src/test/java/org/apache/unomi/services/impl/goals/GoalsServiceImplTest.java b/services/src/test/java/org/apache/unomi/services/impl/goals/GoalsServiceImplTest.java
index 2884b4a60..c19e63973 100644
--- a/services/src/test/java/org/apache/unomi/services/impl/goals/GoalsServiceImplTest.java
+++ b/services/src/test/java/org/apache/unomi/services/impl/goals/GoalsServiceImplTest.java
@@ -54,6 +54,7 @@ import java.io.IOException;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -293,4 +294,10 @@ public class GoalsServiceImplTest {
return null;
});
}
+ @Test
+ public void testGetGoalReportMissingGoalReturnsNull() {
+ assertNull(goalsService.getGoalReport("missing-goal-id"));
+ assertNull(goalsService.getGoalReport("missing-goal-id", null));
+ }
+
}
diff --git a/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java b/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
index 1585e69c6..2df2cd516 100644
--- a/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
+++ b/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
@@ -17,6 +17,9 @@
package org.apache.unomi.services.impl.profiles;
import org.apache.unomi.api.*;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.SegmentService;
import org.apache.unomi.api.services.SchedulerService;
import org.apache.unomi.persistence.spi.PersistenceService;
import org.apache.unomi.persistence.spi.conditions.evaluator.ConditionEvaluatorDispatcher;
@@ -61,6 +64,7 @@ public class ProfileServiceImplTest {
private KarafSecurityService securityService;
private AuditServiceImpl auditService;
private SchedulerService schedulerService;
+ private SegmentService segmentService;
private static final String TENANT_1 = "tenant1";
private static final String SYSTEM_TENANT = "system";
@@ -117,6 +121,7 @@ public class ProfileServiceImplTest {
.thenReturn(Collections.enumeration(Arrays.asList(personasUrl)));
// Set up profile service
+ segmentService = mock(SegmentService.class);
profileService = new ProfileServiceImpl();
profileService.setBundleContext(bundleContext);
profileService.setPersistenceService(persistenceService);
@@ -126,7 +131,7 @@ public class ProfileServiceImplTest {
profileService.setCacheService(multiTypeCacheService);
// Ensure tenantService is available for initial data loading
profileService.setTenantService(tenantService);
-
+ profileService.setSegmentService(segmentService);
profileService.postConstruct();
@@ -671,4 +676,26 @@ public class ProfileServiceImplTest {
}
}
+ @Test
+ public void testExportProfilesPropertiesToCsv_missingSegmentDefinition_usesSegmentId() {
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ Profile profile = new Profile("profile-export-missing-segment");
+ profile.setProperty("firstName", "Jane");
+ profile.getSegments().add("missing-segment-id");
+ profileService.setForceRefreshOnSave(true);
+ profileService.save(profile);
+
+ when(segmentService.getSegmentDefinition("missing-segment-id")).thenReturn(null);
+
+ Query query = new Query();
+
+ String csv = profileService.exportProfilesPropertiesToCsv(query);
+
+ assertNotNull(csv, "CSV export should succeed when segment definition is missing");
+ assertTrue(csv.contains("missing-segment-id"),
+ "CSV should contain the raw segment id when segment metadata is unavailable");
+ return null;
+ });
+ }
+
}