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 <[email protected]> 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; + }); + } + }
