Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions itests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@
<artifactId>unomi-rest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-tracing-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-api</artifactId>
Expand Down
24 changes: 24 additions & 0 deletions itests/src/test/java/org/apache/unomi/itests/EventServiceIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.unomi.itests;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.unomi.api.Event;
import org.apache.unomi.api.PartialList;
import org.apache.unomi.api.Profile;
Expand Down Expand Up @@ -184,4 +185,27 @@ public void test_EventGetNestedProperty() {
Assert.assertEquals(testValue, value);
}

@Test
public void test_DeleteEventViaRest() throws InterruptedException {
String eventId = "test-delete-event-id-" + System.currentTimeMillis();
String profileId = "test-delete-event-profile-id-" + System.currentTimeMillis();
String eventType = "test-delete-event-type";
Profile profile = new Profile(profileId);
Event event = new Event(eventId, eventType, null, profile, null, null, null, new Date());

profileService.save(profile);
keepTrying("Profile with id " + profileId + " not found in the required time", () -> profileService.load(profileId),
Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);

eventService.send(event);
keepTrying("Event has not been raised", () -> eventService.getEvent(eventId), Objects::nonNull, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);

CloseableHttpResponse response = delete("/cxs/events/" + eventId);
Assert.assertEquals("Delete event should return 204 No Content", 204, response.getStatusLine().getStatusCode());

waitForNullValue("Event still present after deletion via REST", () -> eventService.getEvent(eventId), DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,56 @@ public void testEventsCollectorWithPublicApiKey() throws Exception {
}
}

@Test
public void testEventsCollectorWithExplain() throws Exception {
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));

HttpPost request = new HttpPost(getFullUrl(EVENTS_URL + "?explain=true"));
request.addHeader("Content-Type", "application/json");
String requestBody = getObjectMapper().writeValueAsString(eventsCollectorRequest);
request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));

try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, 200, true, true)) {
String responseContent = EntityUtils.toString(response.getEntity());
EventCollectorResponse eventResponse = getObjectMapper().readValue(responseContent, EventCollectorResponse.class);
Assert.assertNotNull("Event collector response should not be null", eventResponse);

int expectedFlags = EventService.SESSION_UPDATED | EventService.PROFILE_UPDATED;
Assert.assertEquals("Response should indicate both session and profile were updated",
expectedFlags, eventResponse.getUpdated());
Assert.assertNotNull("Tracing information should be present", eventResponse.getRequestTracing());
}
}

@Test
public void testEventsCollectorWithExplainUnauthorized() throws Exception {
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));

HttpPost request = new HttpPost(getFullUrl(EVENTS_URL + "?explain=true"));
request.addHeader("Content-Type", "application/json");
String requestBody = getObjectMapper().writeValueAsString(eventsCollectorRequest);
request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));

try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, 403)) {
Assert.assertEquals("Request with explain parameter but without admin privileges should return 403",
403, response.getStatusLine().getStatusCode());
}
}

}
22 changes: 22 additions & 0 deletions itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.unomi.itests;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.unomi.api.*;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.query.Query;
Expand Down Expand Up @@ -535,4 +536,25 @@ public void testPurgeSessions() throws Exception {
}

}

@Test
public void testDeleteSessionViaRest() throws Exception {
String profileId = "test-delete-session-profile-id-" + System.currentTimeMillis();
String sessionId = "test-delete-session-id-" + System.currentTimeMillis();
Profile profile = new Profile(profileId);
profileService.save(profile);
keepTrying("Profile with id " + profileId + " not found in the required time", () -> profileService.load(profileId),
Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);

Session session = new Session(sessionId, profile, new Date(), "test-delete-session-scope");
profileService.saveSession(session);
keepTrying("Session with id " + sessionId + " not found in the required time", () -> profileService.loadSession(sessionId),
Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);

CloseableHttpResponse response = delete("/cxs/profiles/sessions/" + sessionId);
assertEquals("Delete session should return 204 No Content", 204, response.getStatusLine().getStatusCode());

waitForNullValue("Session still present after deletion via REST", () -> profileService.loadSession(sessionId),
DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.unomi.itests.shell;

import org.apache.unomi.api.services.TypeResolutionService;
import org.junit.Assert;
import org.junit.Test;

Expand All @@ -30,10 +31,38 @@ public class OtherCommandsIT extends ShellCommandsBaseIT {
public void testRuleResetStats() throws Exception {
String output = executeCommandAndGetOutput("unomi:rule-reset-stats");
// Should confirm statistics were reset
Assert.assertTrue("Should confirm rule statistics reset",
Assert.assertTrue("Should confirm rule statistics reset",
output.contains("Rule statistics successfully reset"));
}

@Test
public void testListInvalidObjects() throws Exception {
// Seed a genuinely invalid object directly in the tracking service so the command has
// something to report - otherwise this test only ever exercises the "no invalid objects
// found" branch and never validates the summary/table rendering. Going through
// rulesService.setRule() instead is unreliable here: RulesServiceImpl.ensureRuleResolved()
// skips resolution entirely for a rule that's never been seen before (it only resolves
// rules that were already tracked invalid or already had missingPlugins set), so a
// brand-new invalid rule causes setRule() to throw IllegalArgumentException rather than
// being marked invalid.
String objectType = "rules";
String objectId = "test-invalid-rule-" + System.currentTimeMillis();
TypeResolutionService typeResolutionService = definitionsService.getTypeResolutionService();

try {
typeResolutionService.markInvalid(objectType, objectId, "Unresolved condition type: nonExistentConditionType");

String output = executeCommandAndGetOutput("unomi:list-invalid-objects");
Assert.assertTrue("Should show invalid objects summary", output.contains("Invalid Objects Summary"));
Assert.assertTrue("Should report at least one invalid object", output.contains("Total invalid objects:"));
validateNumericValuesInOutput(output, new String[]{"Total invalid objects:"}, false);
validateTableHeaders(output, new String[]{"Object Type", "Object ID"});
Assert.assertTrue("Should list the invalid object's id", output.contains(objectId));
} finally {
typeResolutionService.markValid(objectType, objectId);
}
}

@Test
public void testDeployDefinition() throws Exception {
validateCommandExists("unomi:deploy-definition", "deploy", "definition");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,10 @@ protected Boolean execute(Object... args) throws Exception {
itemType = customItemType;
}
String documentId = getDocumentIDForItemType(itemId, itemType);
String index = getIndexNameForQuery(itemType);
// Single-document delete needs a concrete index/alias, not the wildcard
// pattern getIndexNameForQuery() returns for rollover types (e.g. "context-session-*"),
// which ES's Delete API rejects with index_not_found_exception.
String index = getIndex(itemType);

esClient.delete(DeleteRequest.of(builder -> builder.index(index).id(documentId)));
if (MetadataItem.class.isAssignableFrom(clazz)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.json.Json;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.stream.JsonParser;
Expand Down Expand Up @@ -1398,6 +1399,23 @@ protected Boolean execute(Object... args) throws IOException {
rolloverActionBuilder.add("min_index_age", rolloverMaxAge);
}

// Index patterns for ism_template auto-attachment, using the actual rollover index glob
// (e.g. "context-event-*") rather than raw item type names (e.g. "event") - the latter
// never match real index names, which is why an earlier attempt at ism_template here was
// abandoned in favor of the explicit attachRolloverPolicy() call alone. That explicit call
// only fires when Unomi itself creates the first rollover index (-000001); subsequent
// indices created by ISM's own rollover action never go through Unomi's code path, so they
// only get the inert policy_id setting from the index template and are never actually
// managed. ism_template auto-attaches to any index matching the pattern - including those
// rollover-created indices - so both mechanisms are kept: explicit attach for immediate
// management of the first index, ism_template as the durable fallback for every index after.
JsonArrayBuilder indexPatternsBuilder = Json.createArrayBuilder();
if (rolloverIndices != null) {
for (String rolloverItemType : rolloverIndices) {
indexPatternsBuilder.add(getRolloverIndexForQuery(rolloverItemType));
}
}

// Build the policy from the inside out
JsonObjectBuilder policyBuilder = Json.createObjectBuilder()
.add("states", Json.createArrayBuilder()
Expand All @@ -1408,13 +1426,10 @@ protected Boolean execute(Object... args) throws IOException {
.add("rollover", rolloverActionBuilder)))
.add("transitions", Json.createArrayBuilder())))
.add("default_state", "ingest")
.add("description", "Rollover lifecycle policy");

// Policy attachment is handled deterministically via the plugins.index_state_management.policy_id
// custom setting on each per-item rollover index template (internalCreateRolloverTemplate),
// not via ism_template pattern-matching: rolloverIndices holds item type names (e.g. "event"),
// which never match real index names (e.g. "context-event-000001"), so an ism_template block
// here would never auto-attach the policy.
.add("description", "Rollover lifecycle policy")
.add("ism_template", Json.createObjectBuilder()
.add("index_patterns", indexPatternsBuilder)
.add("priority", 100));

// Wrap the policy and create
JsonObject policyWrapper = Json.createObjectBuilder()
Expand Down
3 changes: 2 additions & 1 deletion rest/postman-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ The collection is organized in a logical demo flow order with 12 main folders:

### 11. Profile Management
- Get, create, update, delete profiles
- Delete sessions
- Search profiles
- Get profile segments and sessions
- **Authentication**: `tenantId:privateApiKey` (Basic Auth)
Expand All @@ -168,7 +169,7 @@ The collection is organized in a logical demo flow order with 12 main folders:

### 12. Additional Useful Requests
- Event type management
- Event search
- Event search and delete
- Condition and action definitions
- Property types
- **Authentication**: `tenantId:privateApiKey` (Basic Auth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ public Event getEvents(@PathParam("id") final String id) {
return eventService.getEvent(id);
}

/**
* Deletes an event by id.
*
* @param id the identifier for the event to delete
*/
@DELETE
@Path("/{id}")
public void deleteEvent(@PathParam("id") final String id) {
eventService.deleteEvent(id);
}

/**
* Retrieves the list of event types identifiers that the server has processed.
* @return a Set of strings that contain event type identifiers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,17 @@ public Session saveSession(Session session) {
return profileService.saveSession(session);
}

/**
* Delete the session by specifying its unique identifier.
*
* @param sessionId the session identifier for the session to delete
*/
@DELETE
@Path("/sessions/{sessionId}")
public void deleteSession(@PathParam("sessionId") String sessionId) {
profileService.deleteSession(sessionId);
}

/**
* Retrieves {@link Event}s for the {@link Session} identified by the provided session identifier, matching any of the provided event types,
* ordered according to the specified {@code sortBy} String and paged: only {@code size} of them are retrieved, starting with the {@code offset}-th one.
Expand Down
Loading
Loading