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 36e9ac4cf [UNOMI-962] Harden 3.1 REST API: V2 compat auth, 
missing-resource 404s, and export validation (#823)
36e9ac4cf is described below

commit 36e9ac4cf49514ecadc81e07c1518e88c3505bc2
Author: Serge Huber <[email protected]>
AuthorDate: Tue Jul 14 06:34:13 2026 +0200

    [UNOMI-962] Harden 3.1 REST API: V2 compat auth, missing-resource 404s, and 
export validation (#823)
    
    REST API Hardening
---
 .cursor/rules/git-town-stack.mdc                   |  33 +++++
 .cursor/rules/jira-tickets.mdc                     |  68 +++++++++
 .cursor/rules/pr-descriptions.mdc                  |  95 +++++++++++++
 extensions/router/router-service/pom.xml           |  15 ++
 .../router/services/ProfileExportServiceImpl.java  |  12 ++
 .../services/ProfileExportServiceImplTest.java     | 105 ++++++++++++++
 .../apache/unomi/itests/V2CompatibilityModeIT.java |  10 +-
 .../ElasticSearchPersistenceServiceImpl.java       |   2 +-
 persistence-opensearch/core/pom.xml                |  15 ++
 .../OpenSearchPersistenceServiceImpl.java          |   2 +-
 .../core/PropertyConditionOSQueryBuilder.java      |   2 +
 .../core/PropertyConditionOSQueryBuilderTest.java  |  68 +++++++++
 .../rest/authentication/AuthenticationFilter.java  |  25 +++-
 .../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 +++-
 22 files changed, 660 insertions(+), 14 deletions(-)

diff --git a/.cursor/rules/git-town-stack.mdc b/.cursor/rules/git-town-stack.mdc
new file mode 100644
index 000000000..162ff6481
--- /dev/null
+++ b/.cursor/rules/git-town-stack.mdc
@@ -0,0 +1,33 @@
+---
+description: Git Town stacked PR workflow — use external git-town-stack for 
merges and tree ops
+alwaysApply: true
+---
+
+# Git Town stack (mandatory)
+
+This repo uses Git Town stacked PRs. Tool lives **outside** this repo (do not 
add scripts/docs here):
+
+```bash
+GTS="${GIT_TOWN_STACK:-${GIT_TOWN_STACK_HOME:-$HOME/projects/sergehuber/git-town-stack}/git-town-stack.sh}"
+cd "$(git rev-parse --show-toplevel)"
+bash "$GTS" doctor
+bash "$GTS" status
+```
+
+**Doctor** also flags unfinished Git Town runstate, mid-rebase, unmerged 
index, index lock.
+
+For merge, rebase, sync, branch delete, or PR base changes on stack branches: 
run **doctor** and **status** first.
+
+**After each squash-merge on GitHub:** `bash "$GTS" after-merge <pr#> 
--dry-run` then `after-merge <pr#>`.
+
+**Routine sync (parent not merged):** `bash "$GTS" sync-stack`
+
+**Never:** delete a remote branch while a child PR uses it as base; blind `git 
town sync --stack` after parent squash-merge; commit `git-town-stack.sh` into 
Unomi PRs.
+
+**Dirty tree:** `git stash push -u` before `git town continue` / sync.
+
+**Stale Git Town runstate** (blocks switch/sync): `git town status reset` if 
stack already fixed; never `git town undo` after good force-push.
+
+**Squash-merge conflicts:** `git rebase --onto master <merged-parent-tip> 
<child>` then `git push --force-with-lease`. Tool docs: 
`~/projects/sergehuber/git-town-stack/docs/KNOWN-ISSUES.md`
+
+**Current stack (verify with status):** master → UNOMI-880 (#763) → UNOMI-139 
(#766). Merge top-down.
diff --git a/.cursor/rules/jira-tickets.mdc b/.cursor/rules/jira-tickets.mdc
new file mode 100644
index 000000000..4eb3fa7fc
--- /dev/null
+++ b/.cursor/rules/jira-tickets.mdc
@@ -0,0 +1,68 @@
+---
+description: JIRA ticket style — problem-focused descriptions, plain-language 
summary, Confluence wiki markup
+alwaysApply: true
+---
+
+# JIRA tickets (Apache Unomi / UNOMI-*)
+
+When the user asks you to **draft, revise, or review a JIRA issue** (title, 
summary, description, acceptance criteria in JIRA), follow this style.
+
+## Summary (plain language)
+
+- One or two sentences a **non-technical reader** can understand (PM, release 
manager, new contributor).
+- State the **user or team pain**, not the fix.
+- Avoid file names, script names, class names, flags, and stack jargon unless 
the user explicitly wants a technical summary.
+
+## Description (problem only)
+
+- Describe **what is wrong today**, **who is affected**, and **why it 
matters**.
+- Use a *What “good” would look like* section for **needs and outcomes** — 
still no implementation.
+- Do **not** include in the main description: proposed scripts, architecture, 
file paths, CLI commands, code snippets, or step-by-step build instructions. 
Put those in a separate sub-task, PR, or “Technical notes” only if the user 
asks.
+
+## Structure (Confluence wiki markup)
+
+Write the description body in **JIRA / Confluence wiki markup** (not 
Markdown), so it can be pasted into JIRA’s wiki editor.
+
+| Element | Markup |
+|---------|--------|
+| Heading 2 | `h2. Section title` |
+| Heading 3 | `h3. Subsection` |
+| Bold | `*text*` |
+| Italic | `_text_` |
+| Bullet list | `* item` (one `*` per line) |
+| Numbered list | `# item` |
+| Horizontal rule | `----` |
+| Inline code | `{{monospace}}` |
+| Code block | `{code}...{code}` |
+| Info panel | `{panel:title=Note|borderStyle=solid}...{panel}` |
+| Link | `[label|https://example.com]` |
+
+## Template
+
+```
+h2. Summary
+
+Plain-language summary here (1–2 sentences).
+
+h2. What is going wrong today
+
+* Bullet describing the current pain
+* Another concrete symptom
+
+h2. Impact
+
+* Who suffers and how (developers, CI, releases, …)
+
+h2. Who is affected
+
+* Role or team
+
+h2. What good would look like
+
+* Observable outcome we need — *not* how to build it
+```
+
+## Title
+
+- Short, problem-oriented (e.g. “Integration test runs lack clear memory and 
resource visibility”).
+- Prefer “lack / hard to / cannot / unreliable” over solution verbs (“Add”, 
“Implement”, “Introduce”) unless the user wants a delivery-style title.
diff --git a/.cursor/rules/pr-descriptions.mdc 
b/.cursor/rules/pr-descriptions.mdc
new file mode 100644
index 000000000..d73c47688
--- /dev/null
+++ b/.cursor/rules/pr-descriptions.mdc
@@ -0,0 +1,95 @@
+---
+description: GitHub PR descriptions — plain-language summary, self-contained 
context, concise structure
+alwaysApply: true
+---
+
+# GitHub pull requests (Apache Unomi)
+
+When the user asks you to **draft, revise, or create a PR description**, 
follow this style. Write for reviewers who may use **English as a second 
language** — see **Simple English** below.
+
+## Simple English (required)
+
+Apply to the **whole** description, not only the plain-language summary.
+
+- Use **short sentences** (one idea each). Prefer **common words** over formal 
or idiomatic ones.
+- Prefer: _fix_, _stop_, _allow_, _check_, _save_, _run_, _before_, _after_. 
Avoid: _leverage_, _remediate_, _no-op_, _land in that window_, _close those 
race windows_ (say _fix the race_ or _prevent the bug_ instead).
+- Use **active voice**: “This PR adds a lock” not “A lock is introduced by 
this PR”.
+- Avoid idioms, slang, and culture-specific phrases (_at the wrong moment_, 
_say the word_, _stand on its own_ → _can be read without other tickets_).
+- Avoid double negatives and nested clauses. Split long sentences into two.
+- Technical terms are fine when needed (scheduler, cache, lock), but explain 
the **effect** in plain words nearby.
+- Bullets: start with a **verb**; keep each bullet to one or two short lines.
+
+## Plain-language summary (required)
+
+- Open with **2–3 sentences** a **non-technical reader** can understand (PM, 
release manager, new contributor).
+- Explain **what Unomi feature or behavior** is affected in everyday terms 
(background jobs, rules/segments cache, REST API, etc.).
+- State the **user or operator pain** (duplicate work, wrong data, failed 
shutdown, flaky CI) — not the fix.
+- Avoid file names, class names, JIRA keys, and stack jargon in this section 
unless the user explicitly wants a technical opener.
+
+## Self-contained (required)
+
+- The description must **stand alone**: a reviewer who has not read linked 
tickets should understand **what changed and why**.
+- Do **not** rely on “see UNOMI-xxx” or “as discussed in …” as the main 
explanation.
+- JIRA keys and issue links belong in a **References** line at the bottom, not 
in the summary.
+
+## Technical summary (required, concise)
+
+- One short section with **3–5 bullets** on the actual fix — enough for a 
developer to orient without reading every commit.
+- Name subsystems (scheduler, cache, REST) not individual methods unless a 
method name is the whole point.
+- Skip commit hashes and per-commit review maps unless the PR is unusually 
large or the user asks for one.
+- Keep the same **Simple English** rules: short lines, clear verbs, no idioms.
+
+## Test plan (required)
+
+- Checklist of what was run or what CI will run.
+- Keep it factual; do not repeat the summary.
+
+## Stacked PRs (when applicable)
+
+This repo uses Git Town stacked PRs (see `.cursor/rules/git-town-stack.mdc`). 
When the PR is **not** based directly on `master`:
+
+- Add a **Stack** section near the top (after the plain-language summary).
+- Link the **parent PR** (the PR whose branch is the base branch) with title 
and number, e.g. `[UNOMI-880 (#763)](https://github.com/apache/unomi/pull/763)`.
+- List any **known child PRs** at write time (same link format). If none exist 
yet, say so explicitly: _No child PRs yet._
+- One sentence on **where this PR sits** in the merge order (e.g. “Merge after 
#763; #766 depends on this branch.”).
+- The plain-language summary and **What changed** must still stand alone — 
stack links are navigation, not a substitute for explaining this PR’s scope.
+
+Verify stack relationships with `bash "$GTS" status` before writing; do not 
guess parent/child numbers.
+
+Skip the Stack section when the PR targets `master` directly and has no stack 
children.
+
+## Structure (Markdown for GitHub)
+
+```markdown
+## Plain-language summary
+
+…
+
+## Stack
+
+- **Parent:** [title (#nnn)](https://github.com/apache/unomi/pull/nnn) — merge 
this PR first
+- **Children:** [title (#mmm)](https://github.com/apache/unomi/pull/mmm) — or 
_None yet_
+- **Order:** …
+
+## What changed
+
+* …
+
+## Test plan
+
+- [ ] …
+
+## References
+
+UNOMI-xxx, UNOMI-yyy
+```
+
+## Title
+
+- Short and outcome-oriented; JIRA keys in the title are fine when the repo 
convention uses them.
+- Prefer describing the fix domain over internal ticket-only wording.
+
+## Conciseness
+
+- Target **~15–25 lines** total for typical bug-fix PRs (add ~3–5 lines when a 
Stack section is needed).
+- Omit anti-patterns: duplicate bullets, “Review map” tables keyed only by 
ticket, empty boilerplate sections.
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/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java 
b/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
index e8318fb5b..cce6c5484 100644
--- a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
@@ -293,9 +293,13 @@ public class V2CompatibilityModeIT extends BaseIT {
         try (CloseableHttpClient adminClient = HttpClients.custom()
                 .setDefaultCredentialsProvider(credsProvider)
                 .setDefaultRequestConfig(requestConfig)
-                .build();
-             CloseableHttpResponse jaasResponse = 
adminClient.execute(getRequest)) {
-            assertEquals("Private endpoint with JAAS auth should work in V2 
compatibility mode", 200, jaasResponse.getStatusLine().getStatusCode());
+                .build()) {
+            try (CloseableHttpResponse jaasResponse = 
adminClient.execute(getRequest)) {
+                assertEquals("Private endpoint with JAAS auth should work in 
V2 compatibility mode", 200, jaasResponse.getStatusLine().getStatusCode());
+            }
+            try (CloseableHttpResponse privacyResponse = 
adminClient.execute(new HttpGet(getFullUrl("/cxs/privacy/info")))) {
+                assertEquals("GET /cxs/privacy/info with Karaf auth should 
work in V2 compatibility mode", 200, 
privacyResponse.getStatusLine().getStatusCode());
+            }
         }
     }
 
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/authentication/AuthenticationFilter.java
 
b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
index b4db2754e..3c59c1cf6 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
@@ -25,7 +25,6 @@ import org.apache.karaf.jaas.boot.principal.RolePrincipal;
 import org.apache.karaf.jaas.boot.principal.UserPrincipal;
 import org.apache.unomi.api.ExecutionContext;
 import org.apache.unomi.api.security.SecurityService;
-import org.apache.unomi.api.security.TenantPrincipal;
 import org.apache.unomi.api.security.UnomiRoles;
 import org.apache.unomi.api.services.ExecutionContextManager;
 import org.apache.unomi.api.tenants.ApiKey;
@@ -47,7 +46,9 @@ import org.apache.commons.lang3.StringUtils;
 import java.io.IOException;
 import java.util.Base64;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A filter that combines JAAS authentication with tenant API key 
authentication:
@@ -310,14 +311,28 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
                 }
                 Subject jaasSubject = ((RolePrefixSecurityContextImpl) 
securityContext).getSubject();
 
-                // Build a merged subject that combines the JAAS principals 
with a TenantPrincipal
-                // for the default tenant, so that resolveTenantId() can find 
it downstream.
+                // Private endpoints in V2 compatibility mode require system 
administrator
+                // authentication (like V2) — a JAAS login alone isn't enough, 
since any Karaf
+                // user (not just admins) can authenticate against the realm.
+                if 
(!securityService.extractRolesFromSubject(jaasSubject).contains(UnomiRoles.ADMINISTRATOR))
 {
+                    logger.debug("V2 compatibility mode: authenticated user 
lacks administrator role, denying access to private endpoint");
+                    unauthorized(requestContext);
+                    return;
+                }
+
+                // Build a merged subject that combines the JAAS principals 
with tenant admin
+                // principals for the default tenant, so that 
resolveTenantId() can find it downstream.
                 String defaultTenantId = 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
                 Subject mergedSubject = new Subject();
                 
mergedSubject.getPrincipals().addAll(jaasSubject.getPrincipals());
                 if (StringUtils.isNotBlank(defaultTenantId)) {
-                    mergedSubject.getPrincipals().add(new 
TenantPrincipal(defaultTenantId));
-                    
executionContextManager.setCurrentContext(executionContextManager.createContext(defaultTenantId));
+                    
mergedSubject.getPrincipals().addAll(securityService.createSubject(defaultTenantId,
 true).getPrincipals());
+                    Set<String> roles = 
securityService.extractRolesFromSubject(mergedSubject);
+                    Set<String> permissions = new HashSet<>();
+                    for (String role : roles) {
+                        
permissions.addAll(securityService.getPermissionsForRole(role));
+                    }
+                    executionContextManager.setCurrentContext(new 
ExecutionContext(defaultTenantId, roles, permissions));
                 } else {
                     
executionContextManager.setCurrentContext(ExecutionContext.systemContext());
                 }
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;
+        });
+    }
+
 }


Reply via email to