From 90909bb6f852e8fccccef7513d8e5ae4b6fde4ae Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Tue, 19 May 2026 15:49:20 +0200 Subject: [PATCH 1/5] UNOMI-139 UNOMI-878: Platform integration tests Add TenantIT, SecurityIT, PersonaIT, EventsCollectorIT, SchedulerIT and persona test fixture. Register platform ITs in AllITs (including TenantIT, SchedulerIT, EventsCollectorIT for CI coverage per split plan). --- .../java/org/apache/unomi/itests/AllITs.java | 5 + .../unomi/itests/EventsCollectorIT.java | 139 ++++ .../org/apache/unomi/itests/PersonaIT.java | 154 +++++ .../org/apache/unomi/itests/SchedulerIT.java | 167 +++++ .../org/apache/unomi/itests/SecurityIT.java | 105 ++++ .../org/apache/unomi/itests/TenantIT.java | 592 ++++++++++++++++++ .../persona-with-sessions-payload.json | 36 ++ 7 files changed, 1198 insertions(+) create mode 100644 itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java create mode 100644 itests/src/test/java/org/apache/unomi/itests/PersonaIT.java create mode 100644 itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java create mode 100644 itests/src/test/java/org/apache/unomi/itests/SecurityIT.java create mode 100644 itests/src/test/java/org/apache/unomi/itests/TenantIT.java create mode 100644 itests/src/test/resources/persona/persona-with-sessions-payload.json diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java index f90a2e8e7..c2f54ce8e 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -38,6 +38,7 @@ ConditionQueryBuilderIT.class, SegmentIT.class, ProfileServiceIT.class, + PersonaIT.class, ProfileImportBasicIT.class, ProfileImportSurfersIT.class, ProfileImportRankingIT.class, @@ -52,6 +53,7 @@ ModifyConsentIT.class, PatchIT.class, ContextServletIT.class, + SecurityIT.class, RuleServiceIT.class, PrivacyServiceIT.class, GroovyActionsServiceIT.class, @@ -75,6 +77,9 @@ OtherCommandsIT.class, HealthCheckIT.class, LegacyQueryBuilderMappingIT.class, + TenantIT.class, + SchedulerIT.class, + EventsCollectorIT.class, }) public class AllITs { } diff --git a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java new file mode 100644 index 000000000..a4cdc3c8c --- /dev/null +++ b/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()); + } + +} diff --git a/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java b/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java new file mode 100644 index 000000000..4e65072d2 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.itests; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.PersonaSession; +import org.apache.unomi.api.PersonaWithSessions; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +/** + * Integration tests for persona functionality. + * This test class covers persona-related features including persona sessions. + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerSuite.class) +public class PersonaIT extends BaseIT { + private static final Logger LOGGER = LoggerFactory.getLogger(PersonaIT.class); + + private static final String BASE_PROFILES_PATH = "/cxs/profiles"; + private static final String PERSONA_WITH_SESSIONS_ENDPOINT = BASE_PROFILES_PATH + "/personasWithSessions"; + private static final String PERSONA_ENDPOINT = BASE_PROFILES_PATH + "/personas"; + private static final String PERSONA_BY_ID_ENDPOINT = PERSONA_ENDPOINT + "/{personaId}"; + private static final String PERSONA_SESSIONS_ENDPOINT = PERSONA_ENDPOINT + "/{personaId}/sessions"; + + private static final String TEST_PERSONA_ID = "test-persona-with-sessions"; + private static final String TEST_SESSION_ID = "test-session-1"; + private static final String PAYLOAD_RESOURCE = "persona/persona-with-sessions-payload.json"; + + @Before + public void setUp() throws InterruptedException { + // Wait for persona REST endpoint to be available + // Using GET /personas/{personaId} with a dummy ID to check endpoint availability + String checkEndpoint = PERSONA_BY_ID_ENDPOINT.replace("{personaId}", "endpoint-check"); + keepTrying("Couldn't find persona endpoint", () -> { + try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(checkEndpoint)), AuthType.JAAS_ADMIN)) { + // Endpoint exists if we get 200 (persona exists), 204 (no content - persona not found), or 404 (not found) + int statusCode = response.getStatusLine().getStatusCode(); + return (statusCode == 200 || statusCode == 204 || statusCode == 404) ? response : null; + } catch (Exception e) { + return null; + } + }, Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + } + + @After + public void tearDown() { + // Clean up: delete the test persona + try { + profileService.delete(TEST_PERSONA_ID, true); + } catch (Exception e) { + LOGGER.warn("Failed to clean up test persona: {}", e.getMessage()); + } + } + + @Test + public void testSavePersonaWithSessionsAndRetrieveSessions() throws Exception { + // Create persona with sessions via REST API + HttpPost createRequest = new HttpPost(getFullUrl(PERSONA_WITH_SESSIONS_ENDPOINT)); + createRequest.setEntity(new StringEntity(resourceAsString(PAYLOAD_RESOURCE), JSON_CONTENT_TYPE)); + + PersonaWithSessions createdPersona; + try (CloseableHttpResponse createResponse = executeHttpRequest(createRequest, AuthType.JAAS_ADMIN)) { + int statusCode = createResponse.getStatusLine().getStatusCode(); + Assert.assertEquals("Persona creation should return 200 OK", 200, statusCode); + + String responseBody = EntityUtils.toString(createResponse.getEntity()); + createdPersona = CustomObjectMapper.getObjectMapper().readValue(responseBody, PersonaWithSessions.class); + } + + Assert.assertNotNull("Created persona should not be null", createdPersona); + Assert.assertNotNull("Created persona should have persona object", createdPersona.getPersona()); + Assert.assertEquals("Persona ID should match", TEST_PERSONA_ID, createdPersona.getPersona().getItemId()); + Assert.assertNotNull("Created persona should have sessions", createdPersona.getSessions()); + Assert.assertFalse("Created persona should have at least one session", createdPersona.getSessions().isEmpty()); + + // Wait for persona's sessions to be indexed before testing session retrieval + // This ensures the sessions are properly linked and queryable + String sessionsUrl = PERSONA_SESSIONS_ENDPOINT.replace("{personaId}", TEST_PERSONA_ID); + PartialList sessions = keepTrying( + "Persona sessions should be retrievable after creation", + () -> { + try { + try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(sessionsUrl)), AuthType.JAAS_ADMIN)) { + if (response.getStatusLine().getStatusCode() == 200) { + String responseBody = EntityUtils.toString(response.getEntity()); + PartialList result = CustomObjectMapper.getObjectMapper().readValue( + responseBody, new TypeReference>() {}); + // Check if the test session is present + if (result != null && result.getList() != null && !result.getList().isEmpty()) { + boolean hasTestSession = result.getList().stream() + .anyMatch(session -> TEST_SESSION_ID.equals(session.getItemId())); + return hasTestSession ? result : null; + } + } + return null; + } + } catch (Exception e) { + LOGGER.debug("Error retrieving persona sessions: {}", e.getMessage()); + return null; + } + }, + Objects::nonNull, + DEFAULT_TRYING_TIMEOUT, + DEFAULT_TRYING_TRIES * 2 // Give more time for indexing + ); + + Assert.assertNotNull("Persona sessions should be retrievable", sessions); + Assert.assertNotNull("Sessions list should not be null", sessions.getList()); + Assert.assertFalse("Sessions list should not be empty", sessions.getList().isEmpty()); + + // Verify the test session is present and properly linked + PersonaSession testSession = sessions.getList().stream() + .filter(session -> TEST_SESSION_ID.equals(session.getItemId())) + .findFirst() + .orElse(null); + + Assert.assertNotNull("Test session should be found in retrieved sessions", testSession); + Assert.assertNotNull("Session should have a profile reference", testSession.getProfile()); + Assert.assertEquals("Session should be linked to the correct persona", TEST_PERSONA_ID, testSession.getProfile().getItemId()); + } +} + diff --git a/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java b/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java new file mode 100644 index 000000000..e7289be69 --- /dev/null +++ b/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 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 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()); + } + + private static class TestTaskExecutor implements TaskExecutor { + static final AtomicBoolean shouldFail = new AtomicBoolean(false); + + @Override + public String getTaskType() { + return TEST_TASK_TYPE; + } + + @Override + public void execute(ScheduledTask task, TaskStatusCallback callback) throws Exception { + if (shouldFail.get()) { + throw new Exception("Test failure"); + } + callback.complete(); + } + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java b/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java new file mode 100644 index 000000000..5e6b51847 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/SecurityIT.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.itests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.unomi.api.ContextRequest; +import org.apache.unomi.api.ExecutionContext; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.security.SecurityService; +import org.apache.unomi.api.security.UnomiRoles; +import org.apache.unomi.api.services.ExecutionContextManager; +import org.apache.unomi.api.services.PersonalizationService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +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.security.auth.Subject; +import java.io.File; +import java.io.IOException; +import java.util.*; + +import static org.junit.Assert.*; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerSuite.class) +public class SecurityIT extends BaseIT { + + private static final String SESSION_ID = "vuln-session-id"; + + private ObjectMapper objectMapper; + + @Before + public void setUp() { + objectMapper = CustomObjectMapper.getObjectMapper(); + } + + @Test + public void testSystemOperationsAndContext() throws Exception { + SecurityService securityService = getOsgiService(SecurityService.class); + ExecutionContextManager contextManager = getOsgiService(ExecutionContextManager.class); + + // Test system subject creation and validation + Subject systemSubject = securityService.getSystemSubject(); + assertNotNull("System subject should not be null", systemSubject); + + Set roles = securityService.extractRolesFromSubject(systemSubject); + assertTrue("System subject should have administrator role", + roles.contains(UnomiRoles.ADMINISTRATOR)); + + // Test system operation execution + String result = contextManager.executeAsSystem(() -> { + ExecutionContext ctx = contextManager.getCurrentContext(); + assertNotNull("System execution context should not be null", ctx); + assertTrue("System context should have admin role", + ctx.hasRole(UnomiRoles.ADMINISTRATOR)); + return "success"; + }); + assertEquals("System operation should execute successfully", "success", result); + + // Test context isolation + ExecutionContext regularContext = contextManager.getCurrentContext(); + assertFalse("Regular context should not have admin role by default", + regularContext.hasRole(UnomiRoles.ADMINISTRATOR)); + + // Test error handling during system operation + try { + contextManager.executeAsSystem(() -> { + throw new RuntimeException("Test exception"); + }); + fail("Should throw exception from system operation"); + } catch (RuntimeException e) { + assertEquals("Test exception", e.getMessage()); + // Verify context is properly restored after exception + ExecutionContext postErrorContext = contextManager.getCurrentContext(); + assertEquals("Context should be restored after error", + regularContext.getTenantId(), postErrorContext.getTenantId()); + } + } + + private TestUtils.RequestResponse executeContextJSONRequest(HttpPost request, String sessionId) throws IOException { + return TestUtils.executeContextJSONRequest(request, sessionId); + } + +} diff --git a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java new file mode 100644 index 000000000..999dca531 --- /dev/null +++ b/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 properties = new HashMap<>(); + properties.put("testProperty", "testValue"); + + Map 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 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())); + } + + @Test + public void testTenantDeletion() throws Exception { + // Create tenants + Tenant tenant = tenantService.createTenant("delete-test", Collections.emptyMap()); + + // Create data for tenants + executionContextManager.executeAsTenant(tenant.getItemId(), () -> { + Profile profile = new Profile(); + profile.setItemId("delete-test-profile"); + persistenceService.save(profile); + }); + + // Delete tenants + tenantService.deleteTenant(tenant.getItemId()); + + // Verify data is inaccessible + Profile loadedProfile = persistenceService.load("delete-test-profile", Profile.class); + Assert.assertNull(loadedProfile); + } + + @Test + public void testCrossSearchPrevention() throws Exception { + // Create two tenants + Tenant tenant1 = tenantService.createTenant("search-test-1", Collections.emptyMap()); + Tenant tenant2 = tenantService.createTenant("search-test-2", Collections.emptyMap()); + + // Add data to tenant1 + executionContextManager.executeAsTenant(tenant1.getItemId(), () -> { + for (int i = 0; i < 10; i++) { + Profile profile = new Profile(); + profile.setItemId("search-test-" + i); + profile.setProperty("testKey", "testValue"); + persistenceService.save(profile); + } + }); + + // Search from tenant2 + executionContextManager.executeAsTenant(tenant2.getItemId(), () -> { + Query query = new Query(); + List results = persistenceService.query("testKey", "testValue", null, Profile.class); + Assert.assertEquals(0, results.size()); + }); + } + + @Test + public void testPublicPrivateApiKeys() throws Exception { + // Create tenant + Tenant tenant = tenantService.createTenant("dual-key-tenant", Collections.emptyMap()); + + // Verify both keys were created during tenant creation + ApiKey publicKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PUBLIC); + ApiKey privateKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PRIVATE); + + Assert.assertNotNull("Public key should exist", publicKey); + Assert.assertNotNull("Private key should exist", privateKey); + Assert.assertEquals("Public key should have correct type", ApiKey.ApiKeyType.PUBLIC, publicKey.getKeyType()); + Assert.assertEquals("Private key should have correct type", ApiKey.ApiKeyType.PRIVATE, privateKey.getKeyType()); + + // Test key type validation + Assert.assertTrue("Public key should validate as public", + tenantService.validateApiKeyWithType(tenant.getItemId(), publicKey.getKey(), ApiKey.ApiKeyType.PUBLIC)); + Assert.assertFalse("Public key should not validate as private", + tenantService.validateApiKeyWithType(tenant.getItemId(), publicKey.getKey(), ApiKey.ApiKeyType.PRIVATE)); + Assert.assertTrue("Private key should validate as private", + tenantService.validateApiKeyWithType(tenant.getItemId(), privateKey.getKey(), ApiKey.ApiKeyType.PRIVATE)); + Assert.assertFalse("Private key should not validate as public", + tenantService.validateApiKeyWithType(tenant.getItemId(), privateKey.getKey(), ApiKey.ApiKeyType.PUBLIC)); + } + + @Test + public void testTenantLookupByApiKey() throws Exception { + // Create tenant + Tenant tenant = tenantService.createTenant("lookup-tenant", Collections.emptyMap()); + ApiKey publicKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PUBLIC); + ApiKey privateKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PRIVATE); + + persistenceService.refresh(); + + // Test lookup by key + Tenant foundByPublic = tenantService.getTenantByApiKey(publicKey.getKey()); + Tenant foundByPrivate = tenantService.getTenantByApiKey(privateKey.getKey()); + + Assert.assertEquals("Should find correct tenant by public key", tenant.getItemId(), foundByPublic.getItemId()); + Assert.assertEquals("Should find correct tenant by private key", tenant.getItemId(), foundByPrivate.getItemId()); + + // Test lookup with type validation + Tenant foundByPublicAsPublic = tenantService.getTenantByApiKey(publicKey.getKey(), ApiKey.ApiKeyType.PUBLIC); + Tenant foundByPublicAsPrivate = tenantService.getTenantByApiKey(publicKey.getKey(), ApiKey.ApiKeyType.PRIVATE); + Tenant foundByPrivateAsPrivate = tenantService.getTenantByApiKey(privateKey.getKey(), ApiKey.ApiKeyType.PRIVATE); + Tenant foundByPrivateAsPublic = tenantService.getTenantByApiKey(privateKey.getKey(), ApiKey.ApiKeyType.PUBLIC); + + Assert.assertNotNull("Should find tenant by public key when type matches", foundByPublicAsPublic); + Assert.assertNull("Should not find tenant by public key when type is private", foundByPublicAsPrivate); + Assert.assertNotNull("Should find tenant by private key when type matches", foundByPrivateAsPrivate); + Assert.assertNull("Should not find tenant by private key when type is public", foundByPrivateAsPublic); + } + + @Test + public void testTenantIdValidation() throws Exception { + // Test tenant ID too long (>32 chars) + try { + tenantService.createTenant("this-tenant-id-is-way-too-long-to-be-valid", Collections.emptyMap()); + Assert.fail("Should reject tenant ID longer than 32 characters"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test tenant ID with invalid characters + try { + tenantService.createTenant("invalid@chars#here", Collections.emptyMap()); + Assert.fail("Should reject tenant ID with invalid characters"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test tenant ID starting with hyphen + try { + tenantService.createTenant("-invalid-start", Collections.emptyMap()); + Assert.fail("Should reject tenant ID starting with hyphen"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test tenant ID ending with hyphen + try { + tenantService.createTenant("invalid-end-", Collections.emptyMap()); + Assert.fail("Should reject tenant ID ending with hyphen"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test tenant ID starting with underscore + try { + tenantService.createTenant("_invalid_start", Collections.emptyMap()); + Assert.fail("Should reject tenant ID starting with underscore"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test tenant ID ending with underscore + try { + tenantService.createTenant("invalid_end_", Collections.emptyMap()); + Assert.fail("Should reject tenant ID ending with underscore"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test system tenant ID + try { + tenantService.createTenant("SYSTEM", Collections.emptyMap()); + Assert.fail("Should reject SYSTEM tenant ID"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Test duplicate tenant ID + Tenant tenant = tenantService.createTenant("valid-tenant", Collections.emptyMap()); + try { + tenantService.createTenant("valid-tenant", Collections.emptyMap()); + Assert.fail("Should reject duplicate tenant ID"); + } catch (IllegalArgumentException e) { + // Expected + } finally { + tenantService.deleteTenant(tenant.getItemId()); + } + + // Test valid tenant ID with hyphens + tenant = tenantService.createTenant("valid-tenant-123", Collections.emptyMap()); + Assert.assertNotNull("Should create tenant with valid ID containing hyphens", tenant); + Assert.assertEquals("Tenant ID should match requested ID", "valid-tenant-123", tenant.getItemId()); + tenantService.deleteTenant(tenant.getItemId()); + + // Test valid tenant ID with underscores + tenant = tenantService.createTenant("valid_tenant_123", Collections.emptyMap()); + Assert.assertNotNull("Should create tenant with valid ID containing underscores", tenant); + Assert.assertEquals("Tenant ID should match requested ID", "valid_tenant_123", tenant.getItemId()); + tenantService.deleteTenant(tenant.getItemId()); + + // Test valid tenant ID with mix of hyphens and underscores + tenant = tenantService.createTenant("valid-tenant_123", Collections.emptyMap()); + Assert.assertNotNull("Should create tenant with valid ID containing both hyphens and underscores", tenant); + Assert.assertEquals("Tenant ID should match requested ID", "valid-tenant_123", tenant.getItemId()); + tenantService.deleteTenant(tenant.getItemId()); + } + + @Test + public void testContextJsonAuthenticationDetection() throws Exception { + // Test that context.json is properly detected as a public endpoint + // This test verifies that the AUTO authentication works correctly + String sessionId = "test-session-" + System.currentTimeMillis(); + try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)), AuthType.AUTO)) { + // Should succeed with public key authentication + Assert.assertEquals("context.json should be accessible with auto-detected public authentication", + 200, response.getStatusLine().getStatusCode()); + } + } +} diff --git a/itests/src/test/resources/persona/persona-with-sessions-payload.json b/itests/src/test/resources/persona/persona-with-sessions-payload.json new file mode 100644 index 000000000..15944f46f --- /dev/null +++ b/itests/src/test/resources/persona/persona-with-sessions-payload.json @@ -0,0 +1,36 @@ +{ + "persona": { + "itemId": "test-persona-with-sessions", + "version": null, + "properties": { + "firstName": "Test", + "lastName": "Persona", + "email": "test.persona@example.com" + }, + "systemProperties": {}, + "segments": [], + "scores": {}, + "mergedWith": null, + "consents": {} + }, + "sessions": [ + { + "itemId": "test-session-1", + "scope": "test", + "profile": { + "itemId": "test-persona-with-sessions", + "properties": { + "firstName": "Test", + "lastName": "Persona" + } + }, + "properties": { + "operatingSystemName": "OS X", + "sessionStartDate": "2024-01-01T00:00:00Z" + }, + "timeStamp": "2024-01-01T00:00:00Z", + "lastEventDate": "2024-01-01T01:00:00Z" + } + ] +} + From 6d0e9fdee01c30086d94b338080e3fa3d6236d11 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Tue, 19 May 2026 16:53:25 +0200 Subject: [PATCH 2/5] UNOMI-139: Fix TenantIT and EventsCollectorIT integration failures - TenantIT: use /cxs/context.json (REST endpoint) instead of legacy /context.json servlet forward that timed out under Pax Exam. - EventCollectorResponse: add Jackson @JsonCreator so IT clients can deserialize the POST /cxs/eventcollector JSON body. --- .../java/org/apache/unomi/itests/TenantIT.java | 8 ++++---- .../unomi/rest/models/EventCollectorResponse.java | 14 +++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java index 999dca531..2c5035dc3 100644 --- a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java @@ -239,12 +239,12 @@ public void testPublicEndpointAuthentication() throws Exception { try { // Test without any authentication String sessionId = "test-session-" + System.currentTimeMillis(); - try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)), AuthType.NONE)) { + try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/cxs/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)); + HttpGet publicRequest = new HttpGet(getFullUrl("/cxs/context.json?sessionId=" + sessionId)); publicRequest.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString( (tenant.getItemId() + ":" + tenant.getPrivateApiKey()).getBytes())); try (CloseableHttpResponse response = executeHttpRequest(publicRequest, AuthType.PRIVATE_KEY)) { @@ -252,7 +252,7 @@ public void testPublicEndpointAuthentication() throws Exception { } // Test with valid public API key (should succeed) - publicRequest = new HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)); + publicRequest = new HttpGet(getFullUrl("/cxs/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()); @@ -583,7 +583,7 @@ public void testContextJsonAuthenticationDetection() throws Exception { // Test that context.json is properly detected as a public endpoint // This test verifies that the AUTO authentication works correctly String sessionId = "test-session-" + System.currentTimeMillis(); - try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)), AuthType.AUTO)) { + try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/cxs/context.json?sessionId=" + sessionId)), AuthType.AUTO)) { // Should succeed with public key authentication Assert.assertEquals("context.json should be accessible with auto-detected public authentication", 200, response.getStatusLine().getStatusCode()); diff --git a/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java b/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java index d1ef9dd3d..eb0fcf777 100644 --- a/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java +++ b/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java @@ -17,6 +17,9 @@ package org.apache.unomi.rest.models; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.io.Serializable; /** @@ -41,15 +44,8 @@ public class EventCollectorResponse implements Serializable { */ private int updated; - public EventCollectorResponse() { - } - - /** - * Creates a new EventCollectorResponse with the specified update flags. - * - * @param updated The bitwise combination of EventService flags indicating what was updated - */ - public EventCollectorResponse(int updated) { + @JsonCreator + public EventCollectorResponse(@JsonProperty("updated") int updated) { this.updated = updated; } From b0f38c1b6b068be72996febfa99d5be98f2b6ced Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Tue, 19 May 2026 18:22:00 +0200 Subject: [PATCH 3/5] Revert "UNOMI-139: Fix TenantIT and EventsCollectorIT integration failures" This reverts commit 958d2fb2f6fecbc4de35addda302eb9f8927a237. --- .../src/test/java/org/apache/unomi/itests/TenantIT.java | 8 ++++---- .../apache/unomi/rest/models/EventCollectorResponse.java | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java index 2c5035dc3..999dca531 100644 --- a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java @@ -239,12 +239,12 @@ public void testPublicEndpointAuthentication() throws Exception { try { // Test without any authentication String sessionId = "test-session-" + System.currentTimeMillis(); - try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/cxs/context.json?sessionId=" + sessionId)), AuthType.NONE)) { + 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("/cxs/context.json?sessionId=" + sessionId)); + 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)) { @@ -252,7 +252,7 @@ public void testPublicEndpointAuthentication() throws Exception { } // Test with valid public API key (should succeed) - publicRequest = new HttpGet(getFullUrl("/cxs/context.json?sessionId=" + sessionId)); + 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()); @@ -583,7 +583,7 @@ public void testContextJsonAuthenticationDetection() throws Exception { // Test that context.json is properly detected as a public endpoint // This test verifies that the AUTO authentication works correctly String sessionId = "test-session-" + System.currentTimeMillis(); - try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/cxs/context.json?sessionId=" + sessionId)), AuthType.AUTO)) { + try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)), AuthType.AUTO)) { // Should succeed with public key authentication Assert.assertEquals("context.json should be accessible with auto-detected public authentication", 200, response.getStatusLine().getStatusCode()); diff --git a/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java b/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java index eb0fcf777..b68e66e4b 100644 --- a/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java +++ b/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java @@ -17,9 +17,6 @@ package org.apache.unomi.rest.models; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.io.Serializable; /** @@ -44,8 +41,7 @@ public class EventCollectorResponse implements Serializable { */ private int updated; - @JsonCreator - public EventCollectorResponse(@JsonProperty("updated") int updated) { + public EventCollectorResponse(int updated) { this.updated = updated; } From 96e93960e186c62a578c40d575abdc6ef362ef56 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 21 May 2026 06:37:19 +0200 Subject: [PATCH 4/5] UNOMI-139: Run HealthCheckIT last in AllITs suite Avoid heavy concurrent /health/check load immediately before TenantIT. --- itests/src/test/java/org/apache/unomi/itests/AllITs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java index c2f54ce8e..b450b3310 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -75,11 +75,11 @@ TenantCommandsIT.class, RuleStatisticsCommandsIT.class, OtherCommandsIT.class, - HealthCheckIT.class, LegacyQueryBuilderMappingIT.class, TenantIT.class, SchedulerIT.class, EventsCollectorIT.class, + HealthCheckIT.class }) public class AllITs { } From afc9d5d1cade5abc0c0593a4716044451db9d4c1 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 10 Jun 2026 08:28:32 +0200 Subject: [PATCH 5/5] UNOMI-139: Fix and harden new integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix several bugs found during review: - TenantIT: two JAAS auth tests were reusing request objects that still had the previous auth header set, so they were not actually testing JAAS — now each JAAS assertion uses a fresh request - TenantIT: apiKey.getItemId() was passed instead of apiKey.getKey() to validateApiKey(), making the expiry test always return false - SchedulerIT: testRetryTask was waiting for the task executor to run and fail organically, but the test node may not be the scheduler executor node — now uses saveTask() to put the task in FAILED state directly before calling the retry endpoint Improve test reliability: - All tests that create tenants now clean up in a finally block, so a test failure does not leave state behind for the next test - HTTP responses are now closed with try-with-resources in all tests - Thread.sleep(1500) replaced with keepTrying() polling Clean up dead code: - Remove unused constants and imports from EventsCollectorIT, SecurityIT, and SchedulerIT - PersonaIT setUp() was returning a closed HTTP response through the keepTrying chain — now returns Boolean.TRUE instead Add no-arg constructor to EventCollectorResponse for Jackson deserialization compatibility --- .../unomi/itests/EventsCollectorIT.java | 41 +- .../org/apache/unomi/itests/PersonaIT.java | 13 +- .../org/apache/unomi/itests/SchedulerIT.java | 47 +-- .../org/apache/unomi/itests/SecurityIT.java | 24 -- .../org/apache/unomi/itests/TenantIT.java | 381 +++++++++--------- .../rest/models/EventCollectorResponse.java | 3 + 6 files changed, 247 insertions(+), 262 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java index a4cdc3c8c..79971ef05 100644 --- a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java @@ -24,9 +24,6 @@ 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; @@ -39,9 +36,7 @@ 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) @@ -52,12 +47,6 @@ public class EventsCollectorIT extends BaseIT { 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 @@ -119,21 +108,23 @@ public void testEventsCollectorWithPublicApiKey() throws Exception { 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. + try (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 — remove auth set by the previous request, clear tenant context + request.removeHeaders("X-Unomi-Api-Key"); HttpClientThatWaitsForUnomi.setTestTenant(null, null, null); - response = HttpClientThatWaitsForUnomi.doRequest(request, 401); - Assert.assertEquals("Request with invalid API key should return 401", 401, response.getStatusLine().getStatusCode()); + try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, 401)) { + Assert.assertEquals("Request with invalid API key should return 401", 401, response.getStatusLine().getStatusCode()); + } } } diff --git a/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java b/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java index 4e65072d2..a5754bcbe 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java @@ -37,6 +37,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.unomi.api.Session; + import java.util.Objects; /** @@ -65,9 +67,8 @@ public void setUp() throws InterruptedException { String checkEndpoint = PERSONA_BY_ID_ENDPOINT.replace("{personaId}", "endpoint-check"); keepTrying("Couldn't find persona endpoint", () -> { try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(checkEndpoint)), AuthType.JAAS_ADMIN)) { - // Endpoint exists if we get 200 (persona exists), 204 (no content - persona not found), or 404 (not found) int statusCode = response.getStatusLine().getStatusCode(); - return (statusCode == 200 || statusCode == 204 || statusCode == 404) ? response : null; + return (statusCode == 200 || statusCode == 204 || statusCode == 404) ? Boolean.TRUE : null; } catch (Exception e) { return null; } @@ -76,7 +77,11 @@ public void setUp() throws InterruptedException { @After public void tearDown() { - // Clean up: delete the test persona + try { + persistenceService.remove(TEST_SESSION_ID, Session.class); + } catch (Exception e) { + LOGGER.warn("Failed to clean up test session: {}", e.getMessage()); + } try { profileService.delete(TEST_PERSONA_ID, true); } catch (Exception e) { @@ -133,7 +138,7 @@ public void testSavePersonaWithSessionsAndRetrieveSessions() throws Exception { }, Objects::nonNull, DEFAULT_TRYING_TIMEOUT, - DEFAULT_TRYING_TRIES * 2 // Give more time for indexing + DEFAULT_TRYING_TRIES * 2 // sessions are indexed asynchronously after persona creation; double retries avoid flakiness ); Assert.assertNotNull("Persona sessions should be retrievable", sessions); diff --git a/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java b/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java index e7289be69..f5d98681b 100644 --- a/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java @@ -17,9 +17,9 @@ package org.apache.unomi.itests; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; 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; @@ -29,13 +29,10 @@ 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.*; @@ -76,7 +73,6 @@ public void setUp() { @After public void tearDown() { - // Clean up test task if (testTaskId != null) { try { schedulerService.cancelTask(testTaskId); @@ -119,8 +115,9 @@ public void testGetNonExistentTask() throws Exception { @Test public void testCancelTask() throws Exception { - CloseableHttpResponse response = delete("/cxs/tasks/" + testTaskId); - assertEquals("Response should be No Content", 204, response.getStatusLine().getStatusCode()); + try (CloseableHttpResponse response = executeHttpRequest(new HttpDelete(getFullUrl("/cxs/tasks/" + testTaskId)))) { + assertEquals("Response should be No Content", 204, response.getStatusLine().getStatusCode()); + } // Verify task is cancelled ScheduledTask task = schedulerService.getTask(testTaskId); @@ -129,28 +126,25 @@ public void testCancelTask() throws Exception { @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 + // Drive the task to FAILED state directly — the test node may not be the scheduler + // executor node, so we cannot rely on the executor running organically + ScheduledTask task = schedulerService.getTask(testTaskId); + task.setStatus(ScheduledTask.TaskStatus.FAILED); + task.setLastError("forced failure for test"); + task.setFailureCount(1); + schedulerService.saveTask(task); + + try (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 retried = objectMapper.readValue(responseBody, ScheduledTask.class); + assertNotNull("Task should not be null", retried); + assertEquals("Task should be scheduled", ScheduledTask.TaskStatus.SCHEDULED, retried.getStatus()); + assertEquals("Failure count should be reset", 0, retried.getFailureCount()); } - - // 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()); } private static class TestTaskExecutor implements TaskExecutor { - static final AtomicBoolean shouldFail = new AtomicBoolean(false); - @Override public String getTaskType() { return TEST_TASK_TYPE; @@ -158,9 +152,6 @@ public String getTaskType() { @Override public void execute(ScheduledTask task, TaskStatusCallback callback) throws Exception { - if (shouldFail.get()) { - throw new Exception("Test failure"); - } callback.complete(); } } diff --git a/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java b/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java index 5e6b51847..df222b61c 100644 --- a/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java @@ -16,19 +16,10 @@ */ package org.apache.unomi.itests; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.unomi.api.ContextRequest; import org.apache.unomi.api.ExecutionContext; -import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.security.SecurityService; import org.apache.unomi.api.security.UnomiRoles; import org.apache.unomi.api.services.ExecutionContextManager; -import org.apache.unomi.api.services.PersonalizationService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.ops4j.pax.exam.junit.PaxExam; @@ -36,8 +27,6 @@ import org.ops4j.pax.exam.spi.reactors.PerSuite; import javax.security.auth.Subject; -import java.io.File; -import java.io.IOException; import java.util.*; import static org.junit.Assert.*; @@ -46,15 +35,6 @@ @ExamReactorStrategy(PerSuite.class) public class SecurityIT extends BaseIT { - private static final String SESSION_ID = "vuln-session-id"; - - private ObjectMapper objectMapper; - - @Before - public void setUp() { - objectMapper = CustomObjectMapper.getObjectMapper(); - } - @Test public void testSystemOperationsAndContext() throws Exception { SecurityService securityService = getOsgiService(SecurityService.class); @@ -98,8 +78,4 @@ public void testSystemOperationsAndContext() throws Exception { } } - private TestUtils.RequestResponse executeContextJSONRequest(HttpPost request, String sessionId) throws IOException { - return TestUtils.executeContextJSONRequest(request, sessionId); - } - } diff --git a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java index 999dca531..bfdf693f1 100644 --- a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java @@ -92,84 +92,92 @@ public void testRestEndpoint() throws Exception { 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); - } + boolean tenantDeleted = false; + try { + // 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()); + 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); + // 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)); + 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); - } + 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()); + 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); + // 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); - } + 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()); + 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(); - } + // 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); - Assert.assertEquals("Delete response should be 204", 204, deleteResponse); + // 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); + tenantDeleted = true; - // Verify tenant is deleted - int verifyDeleteResponse; - try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(REST_ENDPOINT + "/" + updatedTenant.getItemId())), AuthType.JAAS_ADMIN)) { - verifyDeleteResponse = response.getStatusLine().getStatusCode(); + // 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); + } finally { + if (!tenantDeleted) { + try (CloseableHttpResponse r = executeHttpRequest(new HttpDelete(getFullUrl(REST_ENDPOINT + "/" + createdTenant.getItemId())), AuthType.JAAS_ADMIN)) { + // best-effort cleanup + } + } } - - Assert.assertEquals("Get deleted tenant should return 404", 404, verifyDeleteResponse); } @Test @@ -198,32 +206,31 @@ public void testTenantEndpointAuthentication() throws Exception { 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 + try { + // 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()); + } + } finally { + try { tenantService.deleteTenant(tenant.getItemId()); } catch (Exception ignored) {} } } } @@ -258,11 +265,11 @@ public void testPublicEndpointAuthentication() throws Exception { Assert.assertEquals("Valid public API key should grant access to public endpoints", 200, response.getStatusLine().getStatusCode()); } - // Test with JAAS auth (should succeed) + // Test with JAAS auth (should succeed) — use a fresh request to avoid carrying X-Unomi-Api-Key from previous step 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)) { + CloseableHttpResponse response = adminClient.execute(new HttpGet(getFullUrl("/context.json?sessionId=" + sessionId)))) { Assert.assertEquals("JAAS auth should grant access to public endpoints", 200, response.getStatusLine().getStatusCode()); } } finally { @@ -304,11 +311,11 @@ public void testPrivateEndpointAuthentication() throws Exception { Assert.assertEquals("Valid private API key should grant access to private endpoints", 200, response.getStatusLine().getStatusCode()); } - // Test with JAAS auth (should succeed) + // Test with JAAS auth (should succeed) — use a fresh request to avoid carrying Authorization from previous step 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)) { + CloseableHttpResponse response = adminClient.execute(new HttpGet(getFullUrl("/cxs/profiles/count")))) { Assert.assertEquals("JAAS auth should grant access to private endpoints", 200, response.getStatusLine().getStatusCode()); } } finally { @@ -322,23 +329,24 @@ public void testTenantIsolation() throws Exception { 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); - }); + try { + // 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); + }); + } finally { + tenantService.deleteTenant(tenant1.getItemId()); + tenantService.deleteTenant(tenant2.getItemId()); + } } @Test @@ -384,31 +392,34 @@ public void testApiKeyAuthentication() throws Exception { @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())); + try { + ApiKey apiKey = tenantService.generateApiKey(tenant.getItemId(), 1L); // 1ms validity + Thread.sleep(2); // Wait for key to expire + Assert.assertFalse(tenantService.validateApiKey(tenant.getItemId(), apiKey.getKey())); + } finally { + tenantService.deleteTenant(tenant.getItemId()); + } } @Test public void testTenantDeletion() throws Exception { - // Create tenants Tenant tenant = tenantService.createTenant("delete-test", Collections.emptyMap()); - // Create data for tenants - executionContextManager.executeAsTenant(tenant.getItemId(), () -> { - Profile profile = new Profile(); - profile.setItemId("delete-test-profile"); - persistenceService.save(profile); - }); + try { + executionContextManager.executeAsTenant(tenant.getItemId(), () -> { + Profile profile = new Profile(); + profile.setItemId("delete-test-profile"); + persistenceService.save(profile); + }); + } catch (Exception e) { + tenantService.deleteTenant(tenant.getItemId()); + throw e; + } - // Delete tenants + // Deletion is the operation under test tenantService.deleteTenant(tenant.getItemId()); - // Verify data is inaccessible Profile loadedProfile = persistenceService.load("delete-test-profile", Profile.class); Assert.assertNull(loadedProfile); } @@ -419,75 +430,83 @@ public void testCrossSearchPrevention() throws Exception { Tenant tenant1 = tenantService.createTenant("search-test-1", Collections.emptyMap()); Tenant tenant2 = tenantService.createTenant("search-test-2", Collections.emptyMap()); - // Add data to tenant1 - executionContextManager.executeAsTenant(tenant1.getItemId(), () -> { - for (int i = 0; i < 10; i++) { - Profile profile = new Profile(); - profile.setItemId("search-test-" + i); - profile.setProperty("testKey", "testValue"); - persistenceService.save(profile); - } - }); - - // Search from tenant2 - executionContextManager.executeAsTenant(tenant2.getItemId(), () -> { - Query query = new Query(); - List results = persistenceService.query("testKey", "testValue", null, Profile.class); - Assert.assertEquals(0, results.size()); - }); + try { + // Add data to tenant1 + executionContextManager.executeAsTenant(tenant1.getItemId(), () -> { + for (int i = 0; i < 10; i++) { + Profile profile = new Profile(); + profile.setItemId("search-test-" + i); + profile.setProperty("testKey", "testValue"); + persistenceService.save(profile); + } + }); + + // Search from tenant2 + executionContextManager.executeAsTenant(tenant2.getItemId(), () -> { + Query query = new Query(); + List results = persistenceService.query("testKey", "testValue", null, Profile.class); + Assert.assertEquals(0, results.size()); + }); + } finally { + tenantService.deleteTenant(tenant1.getItemId()); + tenantService.deleteTenant(tenant2.getItemId()); + } } @Test public void testPublicPrivateApiKeys() throws Exception { - // Create tenant Tenant tenant = tenantService.createTenant("dual-key-tenant", Collections.emptyMap()); - // Verify both keys were created during tenant creation - ApiKey publicKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PUBLIC); - ApiKey privateKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PRIVATE); - - Assert.assertNotNull("Public key should exist", publicKey); - Assert.assertNotNull("Private key should exist", privateKey); - Assert.assertEquals("Public key should have correct type", ApiKey.ApiKeyType.PUBLIC, publicKey.getKeyType()); - Assert.assertEquals("Private key should have correct type", ApiKey.ApiKeyType.PRIVATE, privateKey.getKeyType()); - - // Test key type validation - Assert.assertTrue("Public key should validate as public", - tenantService.validateApiKeyWithType(tenant.getItemId(), publicKey.getKey(), ApiKey.ApiKeyType.PUBLIC)); - Assert.assertFalse("Public key should not validate as private", - tenantService.validateApiKeyWithType(tenant.getItemId(), publicKey.getKey(), ApiKey.ApiKeyType.PRIVATE)); - Assert.assertTrue("Private key should validate as private", - tenantService.validateApiKeyWithType(tenant.getItemId(), privateKey.getKey(), ApiKey.ApiKeyType.PRIVATE)); - Assert.assertFalse("Private key should not validate as public", - tenantService.validateApiKeyWithType(tenant.getItemId(), privateKey.getKey(), ApiKey.ApiKeyType.PUBLIC)); + try { + ApiKey publicKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PUBLIC); + ApiKey privateKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PRIVATE); + + Assert.assertNotNull("Public key should exist", publicKey); + Assert.assertNotNull("Private key should exist", privateKey); + Assert.assertEquals("Public key should have correct type", ApiKey.ApiKeyType.PUBLIC, publicKey.getKeyType()); + Assert.assertEquals("Private key should have correct type", ApiKey.ApiKeyType.PRIVATE, privateKey.getKeyType()); + + Assert.assertTrue("Public key should validate as public", + tenantService.validateApiKeyWithType(tenant.getItemId(), publicKey.getKey(), ApiKey.ApiKeyType.PUBLIC)); + Assert.assertFalse("Public key should not validate as private", + tenantService.validateApiKeyWithType(tenant.getItemId(), publicKey.getKey(), ApiKey.ApiKeyType.PRIVATE)); + Assert.assertTrue("Private key should validate as private", + tenantService.validateApiKeyWithType(tenant.getItemId(), privateKey.getKey(), ApiKey.ApiKeyType.PRIVATE)); + Assert.assertFalse("Private key should not validate as public", + tenantService.validateApiKeyWithType(tenant.getItemId(), privateKey.getKey(), ApiKey.ApiKeyType.PUBLIC)); + } finally { + tenantService.deleteTenant(tenant.getItemId()); + } } @Test public void testTenantLookupByApiKey() throws Exception { - // Create tenant Tenant tenant = tenantService.createTenant("lookup-tenant", Collections.emptyMap()); - ApiKey publicKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PUBLIC); - ApiKey privateKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PRIVATE); - persistenceService.refresh(); + try { + ApiKey publicKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PUBLIC); + ApiKey privateKey = tenantService.getApiKey(tenant.getItemId(), ApiKey.ApiKeyType.PRIVATE); + + persistenceService.refresh(); - // Test lookup by key - Tenant foundByPublic = tenantService.getTenantByApiKey(publicKey.getKey()); - Tenant foundByPrivate = tenantService.getTenantByApiKey(privateKey.getKey()); + Tenant foundByPublic = tenantService.getTenantByApiKey(publicKey.getKey()); + Tenant foundByPrivate = tenantService.getTenantByApiKey(privateKey.getKey()); - Assert.assertEquals("Should find correct tenant by public key", tenant.getItemId(), foundByPublic.getItemId()); - Assert.assertEquals("Should find correct tenant by private key", tenant.getItemId(), foundByPrivate.getItemId()); + Assert.assertEquals("Should find correct tenant by public key", tenant.getItemId(), foundByPublic.getItemId()); + Assert.assertEquals("Should find correct tenant by private key", tenant.getItemId(), foundByPrivate.getItemId()); - // Test lookup with type validation - Tenant foundByPublicAsPublic = tenantService.getTenantByApiKey(publicKey.getKey(), ApiKey.ApiKeyType.PUBLIC); - Tenant foundByPublicAsPrivate = tenantService.getTenantByApiKey(publicKey.getKey(), ApiKey.ApiKeyType.PRIVATE); - Tenant foundByPrivateAsPrivate = tenantService.getTenantByApiKey(privateKey.getKey(), ApiKey.ApiKeyType.PRIVATE); - Tenant foundByPrivateAsPublic = tenantService.getTenantByApiKey(privateKey.getKey(), ApiKey.ApiKeyType.PUBLIC); + Tenant foundByPublicAsPublic = tenantService.getTenantByApiKey(publicKey.getKey(), ApiKey.ApiKeyType.PUBLIC); + Tenant foundByPublicAsPrivate = tenantService.getTenantByApiKey(publicKey.getKey(), ApiKey.ApiKeyType.PRIVATE); + Tenant foundByPrivateAsPrivate = tenantService.getTenantByApiKey(privateKey.getKey(), ApiKey.ApiKeyType.PRIVATE); + Tenant foundByPrivateAsPublic = tenantService.getTenantByApiKey(privateKey.getKey(), ApiKey.ApiKeyType.PUBLIC); - Assert.assertNotNull("Should find tenant by public key when type matches", foundByPublicAsPublic); - Assert.assertNull("Should not find tenant by public key when type is private", foundByPublicAsPrivate); - Assert.assertNotNull("Should find tenant by private key when type matches", foundByPrivateAsPrivate); - Assert.assertNull("Should not find tenant by private key when type is public", foundByPrivateAsPublic); + Assert.assertNotNull("Should find tenant by public key when type matches", foundByPublicAsPublic); + Assert.assertNull("Should not find tenant by public key when type is private", foundByPublicAsPrivate); + Assert.assertNotNull("Should find tenant by private key when type matches", foundByPrivateAsPrivate); + Assert.assertNull("Should not find tenant by private key when type is public", foundByPrivateAsPublic); + } finally { + tenantService.deleteTenant(tenant.getItemId()); + } } @Test diff --git a/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java b/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java index b68e66e4b..bca06297b 100644 --- a/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java +++ b/rest/src/main/java/org/apache/unomi/rest/models/EventCollectorResponse.java @@ -41,6 +41,9 @@ public class EventCollectorResponse implements Serializable { */ private int updated; + public EventCollectorResponse() { + } + public EventCollectorResponse(int updated) { this.updated = updated; }