Copilot commented on code in PR #766:
URL: https://github.com/apache/unomi/pull/766#discussion_r3317882850


##########
itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java:
##########
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.services.SchedulerService;
+import org.apache.unomi.api.tasks.ScheduledTask;
+import org.apache.unomi.api.tasks.TaskExecutor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
+
+import javax.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.*;
+
+/**
+ * Integration tests for the Scheduler REST API
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class SchedulerIT extends BaseIT {
+
+    private final static String TEST_TASK_TYPE = "test-task";
+    private String testTaskId;
+
+    @Before
+    public void setUp() {
+        // Register a test task executor
+        TestTaskExecutor executor = new TestTaskExecutor();
+        schedulerService.registerTaskExecutor(executor);
+
+        // Create a test task
+        Map<String, Object> parameters = new HashMap<>();
+        parameters.put("testParam", "testValue");
+
+        ScheduledTask task = schedulerService.createTask(
+            TEST_TASK_TYPE,
+            parameters,
+            0,
+            1000,
+            TimeUnit.MILLISECONDS,
+            true,
+            false,
+            false,
+            true
+        );
+        testTaskId = task.getItemId();
+        schedulerService.scheduleTask(task);
+    }
+
+    @After
+    public void tearDown() {
+        // Clean up test task
+        if (testTaskId != null) {
+            try {
+                schedulerService.cancelTask(testTaskId);
+            } catch (Exception e) {
+                // Ignore cleanup errors
+            }
+        }
+    }
+
+    @Test
+    public void testGetTasks() throws Exception {
+        // Test getting all tasks
+        PartialList<ScheduledTask> tasks = get("/cxs/tasks", 
PartialList.class);
+        assertNotNull("Tasks list should not be null", tasks);
+        assertTrue("Should have at least one task", tasks.getList().size() > 
0);
+
+        // Test filtering by status
+        tasks = get("/cxs/tasks?status=SCHEDULED", PartialList.class);
+        assertNotNull("Filtered tasks list should not be null", tasks);
+
+        // Test filtering by type
+        tasks = get("/cxs/tasks?type=" + TEST_TASK_TYPE, PartialList.class);
+        assertNotNull("Type-filtered tasks list should not be null", tasks);
+        assertTrue("Should find test task", tasks.getList().size() > 0);
+    }
+
+    @Test
+    public void testGetTask() throws Exception {
+        ScheduledTask task = get("/cxs/tasks/" + testTaskId, 
ScheduledTask.class);
+        assertNotNull("Task should not be null", task);
+        assertEquals("Task ID should match", testTaskId, task.getItemId());
+        assertEquals("Task type should match", TEST_TASK_TYPE, 
task.getTaskType());
+    }
+
+    @Test
+    public void testGetNonExistentTask() throws Exception {
+        ScheduledTask task = get("/cxs/tasks/non-existent-task", 
ScheduledTask.class);
+        assertNull("Task should be null", task);
+    }
+
+    @Test
+    public void testCancelTask() throws Exception {
+        CloseableHttpResponse response = delete("/cxs/tasks/" + testTaskId);
+        assertEquals("Response should be No Content", 204, 
response.getStatusLine().getStatusCode());
+
+        // Verify task is cancelled
+        ScheduledTask task = schedulerService.getTask(testTaskId);
+        assertEquals("Task should be cancelled", 
ScheduledTask.TaskStatus.CANCELLED, task.getStatus());
+    }
+
+    @Test
+    public void testRetryTask() throws Exception {
+        // First make the task fail
+        TestTaskExecutor.shouldFail.set(true);
+        try {
+            Thread.sleep(1500); // Wait for task to execute and fail
+        } catch (InterruptedException e) {
+            // Ignore
+        }
+
+        // Now retry the task
+        CloseableHttpResponse response = post("/cxs/tasks/" + testTaskId + 
"/retry?resetFailureCount=true", null);
+        assertEquals("Response should be OK", 200, 
response.getStatusLine().getStatusCode());
+
+        String responseBody = EntityUtils.toString(response.getEntity());
+        ScheduledTask task = objectMapper.readValue(responseBody, 
ScheduledTask.class);
+        assertNotNull("Task should not be null", task);
+        assertEquals("Task should be scheduled", 
ScheduledTask.TaskStatus.SCHEDULED, task.getStatus());
+        assertEquals("Failure count should be reset", 0, 
task.getFailureCount());
+    }

Review Comment:
   The response returned by `post(...)` is never closed. In this test suite, 
leaked HTTP responses can accumulate and lead to connection pool exhaustion / 
flaky failures. Wrap the response in try-with-resources.
   



##########
itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java:
##########
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.services.SchedulerService;
+import org.apache.unomi.api.tasks.ScheduledTask;
+import org.apache.unomi.api.tasks.TaskExecutor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
+
+import javax.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.*;
+
+/**
+ * Integration tests for the Scheduler REST API
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class SchedulerIT extends BaseIT {
+
+    private final static String TEST_TASK_TYPE = "test-task";
+    private String testTaskId;
+
+    @Before
+    public void setUp() {
+        // Register a test task executor
+        TestTaskExecutor executor = new TestTaskExecutor();
+        schedulerService.registerTaskExecutor(executor);
+
+        // Create a test task
+        Map<String, Object> parameters = new HashMap<>();
+        parameters.put("testParam", "testValue");
+
+        ScheduledTask task = schedulerService.createTask(
+            TEST_TASK_TYPE,
+            parameters,
+            0,
+            1000,
+            TimeUnit.MILLISECONDS,
+            true,
+            false,
+            false,
+            true
+        );
+        testTaskId = task.getItemId();
+        schedulerService.scheduleTask(task);
+    }
+
+    @After
+    public void tearDown() {
+        // Clean up test task
+        if (testTaskId != null) {
+            try {
+                schedulerService.cancelTask(testTaskId);
+            } catch (Exception e) {
+                // Ignore cleanup errors
+            }
+        }
+    }
+
+    @Test
+    public void testGetTasks() throws Exception {
+        // Test getting all tasks
+        PartialList<ScheduledTask> tasks = get("/cxs/tasks", 
PartialList.class);
+        assertNotNull("Tasks list should not be null", tasks);
+        assertTrue("Should have at least one task", tasks.getList().size() > 
0);
+
+        // Test filtering by status
+        tasks = get("/cxs/tasks?status=SCHEDULED", PartialList.class);
+        assertNotNull("Filtered tasks list should not be null", tasks);
+
+        // Test filtering by type
+        tasks = get("/cxs/tasks?type=" + TEST_TASK_TYPE, PartialList.class);
+        assertNotNull("Type-filtered tasks list should not be null", tasks);
+        assertTrue("Should find test task", tasks.getList().size() > 0);
+    }
+
+    @Test
+    public void testGetTask() throws Exception {
+        ScheduledTask task = get("/cxs/tasks/" + testTaskId, 
ScheduledTask.class);
+        assertNotNull("Task should not be null", task);
+        assertEquals("Task ID should match", testTaskId, task.getItemId());
+        assertEquals("Task type should match", TEST_TASK_TYPE, 
task.getTaskType());
+    }
+
+    @Test
+    public void testGetNonExistentTask() throws Exception {
+        ScheduledTask task = get("/cxs/tasks/non-existent-task", 
ScheduledTask.class);
+        assertNull("Task should be null", task);
+    }
+
+    @Test
+    public void testCancelTask() throws Exception {
+        CloseableHttpResponse response = delete("/cxs/tasks/" + testTaskId);
+        assertEquals("Response should be No Content", 204, 
response.getStatusLine().getStatusCode());
+
+        // Verify task is cancelled
+        ScheduledTask task = schedulerService.getTask(testTaskId);
+        assertEquals("Task should be cancelled", 
ScheduledTask.TaskStatus.CANCELLED, task.getStatus());
+    }

Review Comment:
   `delete(...)` from BaseIT closes the response before returning it, so 
`response` here is already closed. This makes the assertion rely on behavior 
after close and can become brittle; it also hides potential resource leaks if 
the helper changes. Prefer executing the DELETE with `executeHttpRequest(...)` 
and closing it via try-with-resources in the test.
   



##########
itests/src/test/java/org/apache/unomi/itests/TenantIT.java:
##########
@@ -0,0 +1,592 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.tenants.ApiKey;
+import org.apache.unomi.api.tenants.ResourceQuota;
+import org.apache.unomi.api.tenants.Tenant;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.apache.http.util.EntityUtils;
+
+import java.util.*;
+import java.util.Base64;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class TenantIT extends BaseIT {
+
+    private static final String REST_ENDPOINT = "/cxs/tenants";
+    private CustomObjectMapper objectMapper;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        objectMapper = new CustomObjectMapper();
+
+        // Wait for tenant REST endpoint to be available
+        keepTrying("Couldn't find tenant endpoint", () -> {
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.JAAS_ADMIN)) {
+                return response.getStatusLine().getStatusCode() == 200 ? 
response : null;
+            } catch (Exception e) {
+                return null;
+            }
+        }, Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+    }
+
+    @Test
+    public void testRestEndpoint() throws Exception {
+        // Test create tenant
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("testProperty", "testValue");
+
+        Map<String, Object> requestBody = new HashMap<>();
+        requestBody.put("requestedId", "rest-test-tenant");
+        requestBody.put("properties", properties);
+
+        HttpPost createRequest = new HttpPost(getFullUrl(REST_ENDPOINT));
+        createRequest.setEntity(new 
StringEntity(objectMapper.writeValueAsString(requestBody), 
ContentType.APPLICATION_JSON));
+
+        String createResponse;
+        Tenant createdTenant;
+        try (CloseableHttpResponse response = 
executeHttpRequest(createRequest, AuthType.JAAS_ADMIN)) {
+            createResponse = EntityUtils.toString(response.getEntity());
+            createdTenant = objectMapper.readValue(createResponse, 
Tenant.class);
+        }
+
+        Assert.assertNotNull("Created tenant should not be null", 
createdTenant);
+        Assert.assertEquals("rest-test-tenant", createdTenant.getItemId());
+        Assert.assertNotNull("Tenant should have public API key", 
createdTenant.getPublicApiKey());
+        Assert.assertNotNull("Tenant should have private API key", 
createdTenant.getPrivateApiKey());
+
+        // Test get tenant
+        String getResponse;
+        Tenant retrievedTenant;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT + "/" + createdTenant.getItemId())), 
AuthType.JAAS_ADMIN)) {
+            getResponse = EntityUtils.toString(response.getEntity());
+            retrievedTenant = objectMapper.readValue(getResponse, 
Tenant.class);
+        }
+
+        Assert.assertEquals("Retrieved tenant should match created tenant", 
createdTenant.getItemId(), retrievedTenant.getItemId());
+
+        // Test update tenant
+        retrievedTenant.setName("Updated Rest Test Tenant");
+        ResourceQuota quota = new ResourceQuota();
+        quota.setMaxProfiles(1000L);
+        quota.setMaxEvents(5000L);
+        retrievedTenant.setResourceQuota(quota);
+
+        HttpPut updateRequest = new HttpPut(getFullUrl(REST_ENDPOINT + "/" + 
retrievedTenant.getItemId()));
+        updateRequest.setEntity(new 
StringEntity(objectMapper.writeValueAsString(retrievedTenant), 
ContentType.APPLICATION_JSON));
+
+        String updateResponse;
+        Tenant updatedTenant;
+        try (CloseableHttpResponse response = 
executeHttpRequest(updateRequest, AuthType.JAAS_ADMIN)) {
+            updateResponse = EntityUtils.toString(response.getEntity());
+            updatedTenant = objectMapper.readValue(updateResponse, 
Tenant.class);
+        }
+
+        Assert.assertEquals("Tenant name should be updated", "Updated Rest 
Test Tenant", updatedTenant.getName());
+        Assert.assertEquals("Tenant quota should be updated", (Long) 1000L, 
(Long) updatedTenant.getResourceQuota().getMaxProfiles());
+
+        // Test generate new API key
+        String generateKeyUrl = 
String.format("%s/%s/apikeys?type=%s&validityDays=30",
+            getFullUrl(REST_ENDPOINT), updatedTenant.getItemId(), 
ApiKey.ApiKeyType.PUBLIC.name());
+        HttpPost generateKeyRequest = new HttpPost(generateKeyUrl);
+
+        String generateKeyResponse;
+        ApiKey newApiKey;
+        try (CloseableHttpResponse response = 
executeHttpRequest(generateKeyRequest, AuthType.JAAS_ADMIN)) {
+            generateKeyResponse = EntityUtils.toString(response.getEntity());
+            newApiKey = objectMapper.readValue(generateKeyResponse, 
ApiKey.class);
+        }
+
+        Assert.assertNotNull("New API key should not be null", newApiKey);
+        Assert.assertEquals("API key type should match requested type", 
ApiKey.ApiKeyType.PUBLIC, newApiKey.getKeyType());
+
+        // Test validate API key
+        String validateKeyUrl = 
String.format("%s/%s/apikeys/validate?key=%s&type=%s",
+            getFullUrl(REST_ENDPOINT), updatedTenant.getItemId(), 
newApiKey.getKey(), ApiKey.ApiKeyType.PUBLIC.name());
+        int validateResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(validateKeyUrl), AuthType.JAAS_ADMIN)) {
+            validateResponse = response.getStatusLine().getStatusCode();
+        }
+        Assert.assertEquals("API key validation should succeed", 200, 
validateResponse);
+
+        // Test validate with wrong type
+        String validateWrongTypeUrl = 
String.format("%s/%s/apikeys/validate?key=%s&type=%s",
+            getFullUrl(REST_ENDPOINT), updatedTenant.getItemId(), 
newApiKey.getKey(), ApiKey.ApiKeyType.PRIVATE.name());
+        int validateWrongTypeResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(validateWrongTypeUrl), AuthType.JAAS_ADMIN)) {
+            validateWrongTypeResponse = 
response.getStatusLine().getStatusCode();
+        }
+        Assert.assertEquals("API key validation with wrong type should fail", 
401, validateWrongTypeResponse);
+
+        // Test delete tenant
+        int deleteResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpDelete(getFullUrl(REST_ENDPOINT + "/" + updatedTenant.getItemId())), 
AuthType.JAAS_ADMIN)) {
+            deleteResponse = response.getStatusLine().getStatusCode();
+        }
+
+        Assert.assertEquals("Delete response should be 204", 204, 
deleteResponse);
+
+        // Verify tenant is deleted
+        int verifyDeleteResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT + "/" + updatedTenant.getItemId())), 
AuthType.JAAS_ADMIN)) {
+            verifyDeleteResponse = response.getStatusLine().getStatusCode();
+        }
+
+        Assert.assertEquals("Get deleted tenant should return 404", 404, 
verifyDeleteResponse);
+    }
+
+    @Test
+    public void testTenantEndpointAuthentication() throws Exception {
+        // Test without any authentication
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.NONE)) {
+            Assert.assertEquals("Unauthenticated request should be rejected", 
401, response.getStatusLine().getStatusCode());
+        }
+
+        // Create test tenant for API key tests
+        BasicCredentialsProvider adminCredsProvider = new 
BasicCredentialsProvider();
+        adminCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+
+        try (CloseableHttpClient adminClient = 
HttpClients.custom().setDefaultCredentialsProvider(adminCredsProvider).build()) 
{
+            Map<String, Object> requestBody = new HashMap<>();
+            requestBody.put("requestedId", "auth-test-tenant");
+            requestBody.put("properties", Collections.emptyMap());
+
+            HttpPost createRequest = new HttpPost(getFullUrl(REST_ENDPOINT));
+            createRequest.setEntity(new 
StringEntity(objectMapper.writeValueAsString(requestBody), 
ContentType.APPLICATION_JSON));
+
+            String createResponse;
+            Tenant tenant;
+            try (CloseableHttpResponse response = 
adminClient.execute(createRequest)) {
+                createResponse = EntityUtils.toString(response.getEntity());
+                tenant = objectMapper.readValue(createResponse, Tenant.class);
+            }
+
+            // Test with public API key (should fail)
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Public API key should not grant access to 
tenant endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with private API key (should fail)
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Private API key should not grant access 
to tenant endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with invalid JAAS credentials (should fail)
+            BasicCredentialsProvider wrongCredsProvider = new 
BasicCredentialsProvider();
+            wrongCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("wrong", "wrong"));
+            try (CloseableHttpClient wrongClient = 
HttpClients.custom().setDefaultCredentialsProvider(wrongCredsProvider).build();
+                 CloseableHttpResponse response = wrongClient.execute(new 
HttpGet(getFullUrl(REST_ENDPOINT)))) {
+                Assert.assertEquals("Invalid JAAS credentials should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with valid JAAS credentials (should succeed)
+            try (CloseableHttpResponse response = adminClient.execute(new 
HttpGet(getFullUrl(REST_ENDPOINT)))) {
+                Assert.assertEquals("Valid JAAS credentials should be 
accepted", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Cleanup
+            try (CloseableHttpResponse response = adminClient.execute(new 
HttpDelete(getFullUrl(REST_ENDPOINT + "/" + tenant.getItemId())))) {
+                // Response closed automatically
+            }
+        }
+    }
+
+    @Test
+    public void testPublicEndpointAuthentication() throws Exception {
+        // Create test tenant
+        Tenant tenant = tenantService.createTenant("public-test-tenant", 
Collections.emptyMap());
+        
+        // Refresh persistence to ensure tenant is immediately available for 
API key lookup
+        persistenceService.refresh();
+
+        try {
+            // Test without any authentication
+            String sessionId = "test-session-" + System.currentTimeMillis();
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)), AuthType.NONE)) {
+                Assert.assertEquals("Unauthenticated public request should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with private API key (should succeed - private keys have 
higher privileges)
+            HttpGet publicRequest = new 
HttpGet(getFullUrl("/context.json?sessionId=" + sessionId));
+            publicRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":" + 
tenant.getPrivateApiKey()).getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(publicRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Private API key should grant access to 
public endpoints (higher privileges)", 200, 
response.getStatusLine().getStatusCode());
+            }
+
+            // Test with valid public API key (should succeed)
+            publicRequest = new HttpGet(getFullUrl("/context.json?sessionId=" 
+ sessionId));
+            publicRequest.setHeader("X-Unomi-Api-Key", 
tenant.getPublicApiKey());
+            try (CloseableHttpResponse response = 
executeHttpRequest(publicRequest, AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Valid public API key should grant access 
to public endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with JAAS auth (should succeed)
+            BasicCredentialsProvider adminCredsProvider = new 
BasicCredentialsProvider();
+            adminCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+            try (CloseableHttpClient adminClient = 
HttpClients.custom().setDefaultCredentialsProvider(adminCredsProvider).build();
+                 CloseableHttpResponse response = 
adminClient.execute(publicRequest)) {
+                Assert.assertEquals("JAAS auth should grant access to public 
endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+        } finally {
+            tenantService.deleteTenant(tenant.getItemId());
+        }
+    }
+
+    @Test
+    public void testPrivateEndpointAuthentication() throws Exception {
+        // Create test tenant
+        Tenant tenant = tenantService.createTenant("private-test-tenant", 
Collections.emptyMap());
+
+        try {
+            // Test without any authentication
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl("/cxs/profiles/count")), AuthType.NONE)) {
+                Assert.assertEquals("Unauthenticated private request should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with public API key (should fail)
+            HttpGet privateRequest = new 
HttpGet(getFullUrl("/cxs/profiles/count"));
+            privateRequest.setHeader("X-Unomi-Api-Key", 
tenant.getPublicApiKey());
+            try (CloseableHttpResponse response = 
executeHttpRequest(privateRequest, AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Public API key should not grant access to 
private endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with invalid private API key (should fail)
+            privateRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            privateRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":wrong-key").getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(privateRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Invalid private API key should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with valid private API key (should succeed)
+            privateRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            privateRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":" + 
tenant.getPrivateApiKey()).getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(privateRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Valid private API key should grant access 
to private endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with JAAS auth (should succeed)
+            BasicCredentialsProvider adminCredsProvider = new 
BasicCredentialsProvider();
+            adminCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+            try (CloseableHttpClient adminClient = 
HttpClients.custom().setDefaultCredentialsProvider(adminCredsProvider).build();
+                 CloseableHttpResponse response = 
adminClient.execute(privateRequest)) {
+                Assert.assertEquals("JAAS auth should grant access to private 
endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+        } finally {
+            tenantService.deleteTenant(tenant.getItemId());
+        }
+    }
+
+    @Test
+    public void testTenantIsolation() throws Exception {
+        // Create two tenants
+        Tenant tenant1 = tenantService.createTenant("tenant-1", 
Collections.emptyMap());
+        Tenant tenant2 = tenantService.createTenant("tenant-2", 
Collections.emptyMap());
+
+        // Generate API keys
+        ApiKey apiKey1 = tenantService.generateApiKey(tenant1.getItemId(), 
null);
+        ApiKey apiKey2 = tenantService.generateApiKey(tenant2.getItemId(), 
null);
+
+        // Create profile in tenant1
+        executionContextManager.executeAsTenant(tenant1.getItemId(), () -> {
+            Profile profile1 = new Profile();
+            profile1.setItemId("profile1");
+            profile1.setProperty("name", "John");
+            persistenceService.save(profile1);
+        });
+
+        // Try to access profile from tenant2
+        executionContextManager.executeAsTenant(tenant2.getItemId(), () -> {
+            Profile loadedProfile = persistenceService.load("profile1", 
Profile.class);
+            Assert.assertNull("Profile should not be accessible from different 
tenants", loadedProfile);
+        });
+    }

Review Comment:
   This test creates tenants but never deletes them, which can leak state 
across the PerSuite integration test run (Unomi is started once and reused). It 
also generates API keys that are unused. Clean up the created tenants in a 
finally block and drop the unused key generation.
   



##########
itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java:
##########
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.EventsCollectorRequest;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.tenants.ApiKey;
+import org.apache.unomi.api.tenants.Tenant;
+import org.apache.unomi.api.tenants.TenantService;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
+import org.apache.unomi.rest.models.EventCollectorResponse;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+
+import javax.inject.Inject;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Objects;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class EventsCollectorIT extends BaseIT {
+    private final static String EVENTS_URL = "/cxs/eventcollector";
+    private final static String TEST_EVENT_TYPE = "testEventType";
+    private final static String TEST_PROFILE_ID = "test-profile-id";
+    private final static String TEST_SESSION_ID = "test-session-id";
+    private final static String TEST_SCOPE = "testScope";
+    private final static String TEST_TENANT_ID = "test-tenant";
+    private final static String TEST_TENANT_NAME = "Test Tenant";
+    private final static String TEST_TENANT_DESCRIPTION = "Test tenant for 
events collector";
+    private final static String CONTENT_TYPE_HEADER = "Content-Type";
+    private final static String APPLICATION_JSON = "application/json";
+
+    private Profile profile;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        profile = new Profile(TEST_PROFILE_ID);
+        profileService.save(profile);
+
+        keepTrying("Profile " + TEST_PROFILE_ID + " not found in the required 
time", () -> profileService.load(TEST_PROFILE_ID),
+                Objects::nonNull, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
+
+        // create schemas required for tests
+        
schemaService.saveSchema(resourceAsString("schemas/events/test-event-type.json"));
+        keepTrying("Couldn't find json schemas",
+                () -> schemaService.getInstalledJsonSchemaIds(),
+                (schemaIds) -> 
schemaIds.contains("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0";),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        TestUtils.createScope(TEST_SCOPE, "Test scope", scopeService);
+        keepTrying("Scope test-scope not found in the required time", () -> 
scopeService.getScope(TEST_SCOPE),
+                Objects::nonNull, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        persistenceService.refresh();
+        TestUtils.removeAllEvents(definitionsService, persistenceService);
+        TestUtils.removeAllSessions(definitionsService, persistenceService);
+        TestUtils.removeAllProfiles(definitionsService, persistenceService);
+        profileService.delete(profile.getItemId(), false);
+
+        // cleanup schemas
+        
schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0";);
+        keepTrying("Should not find json schemas anymore",
+                () -> schemaService.getInstalledJsonSchemaIds(),
+                (schemaIds) -> 
(!schemaIds.contains("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0";)),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        scopeService.delete(TEST_SCOPE);
+    }
+
+    @Test
+    public void testEventsCollectorWithPublicApiKey() throws Exception {
+
+        // Create event and request
+        Event event = new Event();
+        event.setEventType(TEST_EVENT_TYPE);
+        event.setScope(TEST_SCOPE);
+        event.setSessionId(TEST_SESSION_ID);
+        event.setProfileId(TEST_PROFILE_ID);
+
+        EventsCollectorRequest eventsCollectorRequest = new 
EventsCollectorRequest();
+        eventsCollectorRequest.setSessionId(TEST_SESSION_ID);
+        eventsCollectorRequest.setEvents(Collections.singletonList(event));
+
+        // Send request with public API key
+        HttpPost request = new HttpPost(getFullUrl(EVENTS_URL));
+        request.addHeader("Content-Type", "application/json");
+        String requestBody = 
objectMapper.writeValueAsString(eventsCollectorRequest);
+        request.setEntity(new StringEntity(requestBody, 
ContentType.APPLICATION_JSON));
+
+        // Execute request and verify response
+        CloseableHttpResponse response = 
HttpClientThatWaitsForUnomi.doRequest(request, 200);
+        String responseContent = EntityUtils.toString(response.getEntity());
+        EventCollectorResponse eventResponse = 
objectMapper.readValue(responseContent, EventCollectorResponse.class);
+        Assert.assertNotNull("Event collector response should not be null", 
eventResponse);
+
+        // Check that the response indicates the session and profile were 
updated
+        int expectedFlags = EventService.PROFILE_UPDATED | 
EventService.SESSION_UPDATED;
+        Assert.assertEquals("Response should indicate that the session and 
profile were updated",
+            expectedFlags, eventResponse.getUpdated());
+
+        // Test with invalid API key
+        request.removeHeaders("X-Unomi-Api-Key"); // We need to do this since 
we are reusing the request object since the last call added auth to it.
+        HttpClientThatWaitsForUnomi.setTestTenant(null, null, null);
+        response = HttpClientThatWaitsForUnomi.doRequest(request, 401);
+        Assert.assertEquals("Request with invalid API key should return 401", 
401, response.getStatusLine().getStatusCode());
+    }

Review Comment:
   The `CloseableHttpResponse` returned by 
`HttpClientThatWaitsForUnomi.doRequest(...)` is never closed (twice), which can 
leak connections and make the integration test suite flaky. Wrap both requests 
in try-with-resources; also restore the global `HttpClientThatWaitsForUnomi` 
test tenant after setting it to null for the negative test.
   



##########
itests/src/test/java/org/apache/unomi/itests/TenantIT.java:
##########
@@ -0,0 +1,592 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.tenants.ApiKey;
+import org.apache.unomi.api.tenants.ResourceQuota;
+import org.apache.unomi.api.tenants.Tenant;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.apache.http.util.EntityUtils;
+
+import java.util.*;
+import java.util.Base64;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class TenantIT extends BaseIT {
+
+    private static final String REST_ENDPOINT = "/cxs/tenants";
+    private CustomObjectMapper objectMapper;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        objectMapper = new CustomObjectMapper();
+
+        // Wait for tenant REST endpoint to be available
+        keepTrying("Couldn't find tenant endpoint", () -> {
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.JAAS_ADMIN)) {
+                return response.getStatusLine().getStatusCode() == 200 ? 
response : null;
+            } catch (Exception e) {
+                return null;
+            }
+        }, Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+    }
+
+    @Test
+    public void testRestEndpoint() throws Exception {
+        // Test create tenant
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("testProperty", "testValue");
+
+        Map<String, Object> requestBody = new HashMap<>();
+        requestBody.put("requestedId", "rest-test-tenant");
+        requestBody.put("properties", properties);
+
+        HttpPost createRequest = new HttpPost(getFullUrl(REST_ENDPOINT));
+        createRequest.setEntity(new 
StringEntity(objectMapper.writeValueAsString(requestBody), 
ContentType.APPLICATION_JSON));
+
+        String createResponse;
+        Tenant createdTenant;
+        try (CloseableHttpResponse response = 
executeHttpRequest(createRequest, AuthType.JAAS_ADMIN)) {
+            createResponse = EntityUtils.toString(response.getEntity());
+            createdTenant = objectMapper.readValue(createResponse, 
Tenant.class);
+        }
+
+        Assert.assertNotNull("Created tenant should not be null", 
createdTenant);
+        Assert.assertEquals("rest-test-tenant", createdTenant.getItemId());
+        Assert.assertNotNull("Tenant should have public API key", 
createdTenant.getPublicApiKey());
+        Assert.assertNotNull("Tenant should have private API key", 
createdTenant.getPrivateApiKey());
+
+        // Test get tenant
+        String getResponse;
+        Tenant retrievedTenant;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT + "/" + createdTenant.getItemId())), 
AuthType.JAAS_ADMIN)) {
+            getResponse = EntityUtils.toString(response.getEntity());
+            retrievedTenant = objectMapper.readValue(getResponse, 
Tenant.class);
+        }
+
+        Assert.assertEquals("Retrieved tenant should match created tenant", 
createdTenant.getItemId(), retrievedTenant.getItemId());
+
+        // Test update tenant
+        retrievedTenant.setName("Updated Rest Test Tenant");
+        ResourceQuota quota = new ResourceQuota();
+        quota.setMaxProfiles(1000L);
+        quota.setMaxEvents(5000L);
+        retrievedTenant.setResourceQuota(quota);
+
+        HttpPut updateRequest = new HttpPut(getFullUrl(REST_ENDPOINT + "/" + 
retrievedTenant.getItemId()));
+        updateRequest.setEntity(new 
StringEntity(objectMapper.writeValueAsString(retrievedTenant), 
ContentType.APPLICATION_JSON));
+
+        String updateResponse;
+        Tenant updatedTenant;
+        try (CloseableHttpResponse response = 
executeHttpRequest(updateRequest, AuthType.JAAS_ADMIN)) {
+            updateResponse = EntityUtils.toString(response.getEntity());
+            updatedTenant = objectMapper.readValue(updateResponse, 
Tenant.class);
+        }
+
+        Assert.assertEquals("Tenant name should be updated", "Updated Rest 
Test Tenant", updatedTenant.getName());
+        Assert.assertEquals("Tenant quota should be updated", (Long) 1000L, 
(Long) updatedTenant.getResourceQuota().getMaxProfiles());
+
+        // Test generate new API key
+        String generateKeyUrl = 
String.format("%s/%s/apikeys?type=%s&validityDays=30",
+            getFullUrl(REST_ENDPOINT), updatedTenant.getItemId(), 
ApiKey.ApiKeyType.PUBLIC.name());
+        HttpPost generateKeyRequest = new HttpPost(generateKeyUrl);
+
+        String generateKeyResponse;
+        ApiKey newApiKey;
+        try (CloseableHttpResponse response = 
executeHttpRequest(generateKeyRequest, AuthType.JAAS_ADMIN)) {
+            generateKeyResponse = EntityUtils.toString(response.getEntity());
+            newApiKey = objectMapper.readValue(generateKeyResponse, 
ApiKey.class);
+        }
+
+        Assert.assertNotNull("New API key should not be null", newApiKey);
+        Assert.assertEquals("API key type should match requested type", 
ApiKey.ApiKeyType.PUBLIC, newApiKey.getKeyType());
+
+        // Test validate API key
+        String validateKeyUrl = 
String.format("%s/%s/apikeys/validate?key=%s&type=%s",
+            getFullUrl(REST_ENDPOINT), updatedTenant.getItemId(), 
newApiKey.getKey(), ApiKey.ApiKeyType.PUBLIC.name());
+        int validateResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(validateKeyUrl), AuthType.JAAS_ADMIN)) {
+            validateResponse = response.getStatusLine().getStatusCode();
+        }
+        Assert.assertEquals("API key validation should succeed", 200, 
validateResponse);
+
+        // Test validate with wrong type
+        String validateWrongTypeUrl = 
String.format("%s/%s/apikeys/validate?key=%s&type=%s",
+            getFullUrl(REST_ENDPOINT), updatedTenant.getItemId(), 
newApiKey.getKey(), ApiKey.ApiKeyType.PRIVATE.name());
+        int validateWrongTypeResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(validateWrongTypeUrl), AuthType.JAAS_ADMIN)) {
+            validateWrongTypeResponse = 
response.getStatusLine().getStatusCode();
+        }
+        Assert.assertEquals("API key validation with wrong type should fail", 
401, validateWrongTypeResponse);
+
+        // Test delete tenant
+        int deleteResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpDelete(getFullUrl(REST_ENDPOINT + "/" + updatedTenant.getItemId())), 
AuthType.JAAS_ADMIN)) {
+            deleteResponse = response.getStatusLine().getStatusCode();
+        }
+
+        Assert.assertEquals("Delete response should be 204", 204, 
deleteResponse);
+
+        // Verify tenant is deleted
+        int verifyDeleteResponse;
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT + "/" + updatedTenant.getItemId())), 
AuthType.JAAS_ADMIN)) {
+            verifyDeleteResponse = response.getStatusLine().getStatusCode();
+        }
+
+        Assert.assertEquals("Get deleted tenant should return 404", 404, 
verifyDeleteResponse);
+    }
+
+    @Test
+    public void testTenantEndpointAuthentication() throws Exception {
+        // Test without any authentication
+        try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.NONE)) {
+            Assert.assertEquals("Unauthenticated request should be rejected", 
401, response.getStatusLine().getStatusCode());
+        }
+
+        // Create test tenant for API key tests
+        BasicCredentialsProvider adminCredsProvider = new 
BasicCredentialsProvider();
+        adminCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+
+        try (CloseableHttpClient adminClient = 
HttpClients.custom().setDefaultCredentialsProvider(adminCredsProvider).build()) 
{
+            Map<String, Object> requestBody = new HashMap<>();
+            requestBody.put("requestedId", "auth-test-tenant");
+            requestBody.put("properties", Collections.emptyMap());
+
+            HttpPost createRequest = new HttpPost(getFullUrl(REST_ENDPOINT));
+            createRequest.setEntity(new 
StringEntity(objectMapper.writeValueAsString(requestBody), 
ContentType.APPLICATION_JSON));
+
+            String createResponse;
+            Tenant tenant;
+            try (CloseableHttpResponse response = 
adminClient.execute(createRequest)) {
+                createResponse = EntityUtils.toString(response.getEntity());
+                tenant = objectMapper.readValue(createResponse, Tenant.class);
+            }
+
+            // Test with public API key (should fail)
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Public API key should not grant access to 
tenant endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with private API key (should fail)
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Private API key should not grant access 
to tenant endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with invalid JAAS credentials (should fail)
+            BasicCredentialsProvider wrongCredsProvider = new 
BasicCredentialsProvider();
+            wrongCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("wrong", "wrong"));
+            try (CloseableHttpClient wrongClient = 
HttpClients.custom().setDefaultCredentialsProvider(wrongCredsProvider).build();
+                 CloseableHttpResponse response = wrongClient.execute(new 
HttpGet(getFullUrl(REST_ENDPOINT)))) {
+                Assert.assertEquals("Invalid JAAS credentials should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with valid JAAS credentials (should succeed)
+            try (CloseableHttpResponse response = adminClient.execute(new 
HttpGet(getFullUrl(REST_ENDPOINT)))) {
+                Assert.assertEquals("Valid JAAS credentials should be 
accepted", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Cleanup
+            try (CloseableHttpResponse response = adminClient.execute(new 
HttpDelete(getFullUrl(REST_ENDPOINT + "/" + tenant.getItemId())))) {
+                // Response closed automatically
+            }
+        }
+    }
+
+    @Test
+    public void testPublicEndpointAuthentication() throws Exception {
+        // Create test tenant
+        Tenant tenant = tenantService.createTenant("public-test-tenant", 
Collections.emptyMap());
+        
+        // Refresh persistence to ensure tenant is immediately available for 
API key lookup
+        persistenceService.refresh();
+
+        try {
+            // Test without any authentication
+            String sessionId = "test-session-" + System.currentTimeMillis();
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)), AuthType.NONE)) {
+                Assert.assertEquals("Unauthenticated public request should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with private API key (should succeed - private keys have 
higher privileges)
+            HttpGet publicRequest = new 
HttpGet(getFullUrl("/context.json?sessionId=" + sessionId));
+            publicRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":" + 
tenant.getPrivateApiKey()).getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(publicRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Private API key should grant access to 
public endpoints (higher privileges)", 200, 
response.getStatusLine().getStatusCode());
+            }
+
+            // Test with valid public API key (should succeed)
+            publicRequest = new HttpGet(getFullUrl("/context.json?sessionId=" 
+ sessionId));
+            publicRequest.setHeader("X-Unomi-Api-Key", 
tenant.getPublicApiKey());
+            try (CloseableHttpResponse response = 
executeHttpRequest(publicRequest, AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Valid public API key should grant access 
to public endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with JAAS auth (should succeed)
+            BasicCredentialsProvider adminCredsProvider = new 
BasicCredentialsProvider();
+            adminCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+            try (CloseableHttpClient adminClient = 
HttpClients.custom().setDefaultCredentialsProvider(adminCredsProvider).build();
+                 CloseableHttpResponse response = 
adminClient.execute(publicRequest)) {
+                Assert.assertEquals("JAAS auth should grant access to public 
endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+        } finally {
+            tenantService.deleteTenant(tenant.getItemId());
+        }
+    }
+
+    @Test
+    public void testPrivateEndpointAuthentication() throws Exception {
+        // Create test tenant
+        Tenant tenant = tenantService.createTenant("private-test-tenant", 
Collections.emptyMap());
+
+        try {
+            // Test without any authentication
+            try (CloseableHttpResponse response = executeHttpRequest(new 
HttpGet(getFullUrl("/cxs/profiles/count")), AuthType.NONE)) {
+                Assert.assertEquals("Unauthenticated private request should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with public API key (should fail)
+            HttpGet privateRequest = new 
HttpGet(getFullUrl("/cxs/profiles/count"));
+            privateRequest.setHeader("X-Unomi-Api-Key", 
tenant.getPublicApiKey());
+            try (CloseableHttpResponse response = 
executeHttpRequest(privateRequest, AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Public API key should not grant access to 
private endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with invalid private API key (should fail)
+            privateRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            privateRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":wrong-key").getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(privateRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Invalid private API key should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with valid private API key (should succeed)
+            privateRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            privateRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":" + 
tenant.getPrivateApiKey()).getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(privateRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Valid private API key should grant access 
to private endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with JAAS auth (should succeed)
+            BasicCredentialsProvider adminCredsProvider = new 
BasicCredentialsProvider();
+            adminCredsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+            try (CloseableHttpClient adminClient = 
HttpClients.custom().setDefaultCredentialsProvider(adminCredsProvider).build();
+                 CloseableHttpResponse response = 
adminClient.execute(privateRequest)) {
+                Assert.assertEquals("JAAS auth should grant access to private 
endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+        } finally {
+            tenantService.deleteTenant(tenant.getItemId());
+        }
+    }
+
+    @Test
+    public void testTenantIsolation() throws Exception {
+        // Create two tenants
+        Tenant tenant1 = tenantService.createTenant("tenant-1", 
Collections.emptyMap());
+        Tenant tenant2 = tenantService.createTenant("tenant-2", 
Collections.emptyMap());
+
+        // Generate API keys
+        ApiKey apiKey1 = tenantService.generateApiKey(tenant1.getItemId(), 
null);
+        ApiKey apiKey2 = tenantService.generateApiKey(tenant2.getItemId(), 
null);
+
+        // Create profile in tenant1
+        executionContextManager.executeAsTenant(tenant1.getItemId(), () -> {
+            Profile profile1 = new Profile();
+            profile1.setItemId("profile1");
+            profile1.setProperty("name", "John");
+            persistenceService.save(profile1);
+        });
+
+        // Try to access profile from tenant2
+        executionContextManager.executeAsTenant(tenant2.getItemId(), () -> {
+            Profile loadedProfile = persistenceService.load("profile1", 
Profile.class);
+            Assert.assertNull("Profile should not be accessible from different 
tenants", loadedProfile);
+        });
+    }
+
+    @Test
+    public void testApiKeyAuthentication() throws Exception {
+        // Create test tenant
+        Tenant tenant = tenantService.createTenant("test-tenant-auth", 
Collections.emptyMap());
+
+        try {
+            // Test with private API key (should succeed)
+            ApiKey privateKey = 
tenantService.generateApiKeyWithType(tenant.getItemId(), 
ApiKey.ApiKeyType.PRIVATE, null);
+            HttpGet getRequest = new 
HttpGet(getFullUrl("/cxs/profiles/count"));
+            getRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+                (tenant.getItemId() + ":" + privateKey.getKey()).getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(getRequest, AuthType.PRIVATE_KEY)) {
+                Assert.assertEquals("Private API key should grant access to 
private endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with JAAS authentication (should succeed)
+            getRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            getRequest.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(("karaf:karaf").getBytes()));
+            try (CloseableHttpResponse response = 
executeHttpRequest(getRequest, AuthType.JAAS_ADMIN)) {
+                Assert.assertEquals("JAAS authentication should grant access 
to private endpoints", 200, response.getStatusLine().getStatusCode());
+            }
+
+            // Test with public API key (should fail)
+            ApiKey publicKey = 
tenantService.generateApiKeyWithType(tenant.getItemId(), 
ApiKey.ApiKeyType.PUBLIC, null);
+            getRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            getRequest.setHeader("X-Unomi-Api-Key", publicKey.getKey());
+            try (CloseableHttpResponse response = 
executeHttpRequest(getRequest, AuthType.PUBLIC_KEY)) {
+                Assert.assertEquals("Public API key should not grant access to 
private endpoints", 401, response.getStatusLine().getStatusCode());
+            }
+
+            // Test without any authentication (should fail)
+            getRequest = new HttpGet(getFullUrl("/cxs/profiles/count"));
+            try (CloseableHttpResponse response = 
executeHttpRequest(getRequest, AuthType.NONE)) {
+                Assert.assertEquals("Unauthenticated request should be 
rejected", 401, response.getStatusLine().getStatusCode());
+            }
+        } finally {
+            // Cleanup
+            tenantService.deleteTenant(tenant.getItemId());
+        }
+    }
+
+    @Test
+    public void testExpiredApiKey() throws Exception {
+        // Create tenants with short-lived API key
+        Tenant tenant = tenantService.createTenant("expired-tenant", 
Collections.emptyMap());
+        ApiKey apiKey = tenantService.generateApiKey(tenant.getItemId(), 1L); 
// 1ms validity
+
+        Thread.sleep(2); // Wait for key to expire
+
+        Assert.assertFalse(tenantService.validateApiKey(tenant.getItemId(), 
apiKey.getItemId()));
+    }

Review Comment:
   `validateApiKey(...)` expects the API key *value*, but the test passes 
`apiKey.getItemId()`. Since itemId is not the secret key, this assertion will 
be false regardless of whether the key actually expired, making the test 
ineffective. Use `apiKey.getKey()` and clean up the created tenant.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to