diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java index dc67fb70ce47..fefdd239ba28 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -144,12 +145,8 @@ public static boolean shouldTriggerAlert(ChangeEvent event, FilteringRules confi } // Trigger Specific Settings - if (event.getEntityType().equals(THREAD) - && (config.getResources().get(0).equals("announcement") - || config.getResources().get(0).equals("task") - || config.getResources().get(0).equals("conversation"))) { - Thread thread = AlertsRuleEvaluator.getThread(event); - return config.getResources().get(0).equalsIgnoreCase(thread.getType().value()); + if (event.getEntityType().equals(THREAD)) { + return shouldTriggerAlertForThread(event, config.getResources().get(0)); } // Test Suite @@ -165,6 +162,22 @@ public static boolean shouldTriggerAlert(ChangeEvent event, FilteringRules confi return config.getResources().contains(event.getEntityType()); // Use Trigger Specific Settings } + private static final Set THREAD_TYPE_RESOURCES = + Set.of("announcement", "task", "conversation"); + + private static boolean shouldTriggerAlertForThread(ChangeEvent event, String resource) { + Thread thread = AlertsRuleEvaluator.getThread(event); + if (thread == null) { + return false; + } + if (THREAD_TYPE_RESOURCES.contains(resource.toLowerCase(Locale.ROOT))) { + return resource.equalsIgnoreCase(thread.getType().value()); + } + // Entity-type resource (e.g., "glossaryTerm"): match threads whose parent entity type matches + return thread.getEntityRef() != null + && resource.equalsIgnoreCase(thread.getEntityRef().getType()); + } + public static SubscriptionStatus buildSubscriptionStatus( SubscriptionStatus.Status status, Long lastSuccessful, diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertUtilTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertUtilTest.java index 9740b2c3bd75..c47cc6ad1936 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertUtilTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertUtilTest.java @@ -1,9 +1,20 @@ package org.openmetadata.service.events.subscription; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Collections; import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.Test; +import org.openmetadata.schema.entity.events.FilteringRules; +import org.openmetadata.schema.entity.feed.Thread; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.EventType; +import org.openmetadata.schema.type.ThreadType; +import org.openmetadata.service.Entity; class AlertUtilTest { @@ -67,4 +78,189 @@ void testConvertInputListToString_consecutiveQuotes() { String result = AlertUtil.convertInputListToString(input); assertEquals("'test''''value'", result); } + + // ---- shouldTriggerAlert: null / "all" resource ---------------------------- + + @Test + void shouldTriggerAlert_nullConfig_returnsTrue() { + ChangeEvent event = entityChangeEvent("table"); + assertTrue(AlertUtil.shouldTriggerAlert(event, null)); + } + + @Test + void shouldTriggerAlert_allResource_returnsTrue() { + ChangeEvent event = entityChangeEvent("glossaryTerm"); + FilteringRules config = filteringRules("all"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + // ---- shouldTriggerAlert: entity change events ---------------------------- + + @Test + void shouldTriggerAlert_entityEvent_matchingResource_returnsTrue() { + ChangeEvent event = entityChangeEvent("glossaryTerm"); + FilteringRules config = filteringRules("glossaryTerm"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_entityEvent_nonMatchingResource_returnsFalse() { + ChangeEvent event = entityChangeEvent("table"); + FilteringRules config = filteringRules("glossaryTerm"); + assertFalse(AlertUtil.shouldTriggerAlert(event, config)); + } + + // ---- shouldTriggerAlert: thread-type resource ("conversation" etc.) ------ + + @Test + void shouldTriggerAlert_conversationThread_conversationResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(glossaryTermRef()); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_CREATED); + FilteringRules config = filteringRules("conversation"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_taskThread_conversationResource_returnsFalse() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Task) + .withEntityRef(glossaryTermRef()); + ChangeEvent event = threadChangeEvent(thread, EventType.TASK_CREATED); + FilteringRules config = filteringRules("conversation"); + assertFalse(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_taskThread_taskResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Task) + .withEntityRef(glossaryTermRef()); + ChangeEvent event = threadChangeEvent(thread, EventType.TASK_CREATED); + FilteringRules config = filteringRules("task"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_announcementThread_announcementResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Announcement) + .withEntityRef(new EntityReference().withId(UUID.randomUUID()).withType("table")); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_CREATED); + FilteringRules config = filteringRules("announcement"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + // ---- shouldTriggerAlert: entity-type resource for thread events ---------- + // These are the bug cases: thread events on a GlossaryTerm should fire + // when the subscription resource is "glossaryTerm", not just "conversation". + + @Test + void shouldTriggerAlert_conversationOnGlossaryTerm_glossaryTermResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(glossaryTermRef()); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_CREATED); + FilteringRules config = filteringRules("glossaryTerm"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_threadUpdateOnGlossaryTerm_glossaryTermResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(glossaryTermRef()); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_UPDATED); + FilteringRules config = filteringRules("glossaryTerm"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_postCreatedOnGlossaryTerm_glossaryTermResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(glossaryTermRef()); + ChangeEvent event = threadChangeEvent(thread, EventType.POST_CREATED); + FilteringRules config = filteringRules("glossaryTerm"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_threadOnTable_glossaryTermResource_returnsFalse() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(new EntityReference().withId(UUID.randomUUID()).withType("table")); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_CREATED); + FilteringRules config = filteringRules("glossaryTerm"); + assertFalse(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_threadOnTable_tableResource_returnsTrue() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(new EntityReference().withId(UUID.randomUUID()).withType("table")); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_CREATED); + FilteringRules config = filteringRules("table"); + assertTrue(AlertUtil.shouldTriggerAlert(event, config)); + } + + @Test + void shouldTriggerAlert_threadWithNullEntityRef_entityTypeResource_returnsFalse() { + Thread thread = + new Thread() + .withId(UUID.randomUUID()) + .withType(ThreadType.Conversation) + .withEntityRef(null); + ChangeEvent event = threadChangeEvent(thread, EventType.THREAD_CREATED); + FilteringRules config = filteringRules("glossaryTerm"); + assertFalse(AlertUtil.shouldTriggerAlert(event, config)); + } + + // ---- helpers --------------------------------------------------------------- + + private static ChangeEvent entityChangeEvent(String entityType) { + return new ChangeEvent() + .withId(UUID.randomUUID()) + .withEventType(EventType.ENTITY_UPDATED) + .withEntityType(entityType); + } + + private static ChangeEvent threadChangeEvent(Thread thread, EventType eventType) { + return new ChangeEvent() + .withId(UUID.randomUUID()) + .withEventType(eventType) + .withEntityType(Entity.THREAD) + .withEntity(thread); + } + + private static FilteringRules filteringRules(String resource) { + return new FilteringRules() + .withResources(List.of(resource)) + .withRules(Collections.emptyList()) + .withActions(Collections.emptyList()); + } + + private static EntityReference glossaryTermRef() { + return new EntityReference().withId(UUID.randomUUID()).withType("glossaryTerm"); + } }