From 46cf15c12ecc849b931bcd8d1eceb741171db1c1 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Tue, 29 Oct 2024 15:29:23 +0530 Subject: [PATCH] GEN 1931 - Fix entity link accepted chars (#18391) --- .../service/resources/EntityResourceTest.java | 4 +- .../dqtests/TestCaseResourceTest.java | 182 ++++-------------- .../events/EventSubscriptionResourceTest.java | 146 ++++++++------ .../resources/feeds/FeedResourceTest.java | 3 +- .../feeds/SuggestionsResourceTest.java | 2 +- .../service/util/EntityUtilTest.java | 166 ++++++++++++++++ .../resources/json/schema/type/basic.json | 2 +- .../components/AddTestSuitePipeline.tsx | 5 +- 8 files changed, 300 insertions(+), 210 deletions(-) diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index a2ca45588e5..58b35ae3315 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -236,7 +236,7 @@ public abstract class EntityResourceTest$\"]"; + "[entityLink must match \"(?U)^<#E::\\w+::(?:[^:<>|]|:[^:<>|])+(?:::(?:[^:<>|]|:[^:<>|])+)*>$\"]"; // Random unicode string generator to test entity name accepts all the unicode characters protected static final RandomStringGenerator RANDOM_STRING_GENERATOR = diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java index ff8fe94b451..aa1eaceac5f 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.openmetadata.common.utils.CommonUtil.listOf; import static org.openmetadata.schema.type.ColumnDataType.BIGINT; @@ -2572,152 +2573,53 @@ public class TestCaseResourceTest extends EntityResourceTest columns = Arrays.asList(C1, C2, C3); + void test_testCaseInvalidEntityLinkTest(TestInfo testInfo) throws IOException { + // Invalid entity link as not parsable by antlr parser + String entityLink = "<#E::table::special!@#$%^&*()_+[]{}|;:\\'\",./?>"; + CreateTestCase create = createRequest(testInfo); + create + .withEntityLink(entityLink) + .withTestSuite(TEST_SUITE1.getFullyQualifiedName()) + .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) + .withParameterValues( + List.of(new TestCaseParameterValue().withValue("100").withName("missingCountValue"))); - // Add 3 rows of sample data for 3 columns - List> rows = - Arrays.asList( - Arrays.asList("c1Value1", 1, true), - Arrays.asList("c1Value2", null, false), - Arrays.asList("c1Value3", 3, true)); + assertThrows( + HttpResponseException.class, + () -> createAndCheckEntity(create, ADMIN_AUTH_HEADERS), + "entityLink must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\""); - putTestCaseResult( - testCase.getFullyQualifiedName(), - new TestCaseResult() - .withResult("result") - .withTestCaseStatus(TestCaseStatus.Failed) - .withTimestamp(TestUtils.dateToTimestamp("2024-01-01")), - ADMIN_AUTH_HEADERS); + entityLink = "<#E::table::user::column>"; + create.setEntityLink(entityLink); + assertThrows( + HttpResponseException.class, + () -> createAndCheckEntity(create, ADMIN_AUTH_HEADERS), + "entityLink must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\""); - putFailedRowsSample(testCase, columns, rows, ADMIN_AUTH_HEADERS); + entityLink = "<#E::table::user>name::column>"; + create.setEntityLink(entityLink); + assertThrows( + HttpResponseException.class, + () -> createAndCheckEntity(create, ADMIN_AUTH_HEADERS), + "entityLink must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\""); - // resolving test case deletes the sample data - TestCaseResult testCaseResult = - new TestCaseResult() - .withResult("tested") - .withTestCaseStatus(TestCaseStatus.Success) - .withTimestamp(TestUtils.dateToTimestamp("2021-09-09")); - putTestCaseResult(testCase.getFullyQualifiedName(), testCaseResult, ADMIN_AUTH_HEADERS); - assertResponse( - () -> getSampleData(testCase.getId(), ADMIN_AUTH_HEADERS), - NOT_FOUND, - FAILED_ROWS_SAMPLE_EXTENSION + " instance for " + testCase.getId() + " not found"); + entityLink = "<#E::table::foo<>bar::baz>\");"; + create.setEntityLink(entityLink); + assertThrows( + HttpResponseException.class, + () -> createAndCheckEntity(create, ADMIN_AUTH_HEADERS), + "entityLink must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\""); + + entityLink = "<#E::table::::baz>"; + create.setEntityLink(entityLink); + assertThrows( + HttpResponseException.class, + () -> createAndCheckEntity(create, ADMIN_AUTH_HEADERS), + "entityLink must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\""); } - @Test - void test_sensitivePIISampleData(TestInfo test) throws IOException, ParseException { - // Create table with owner and a column tagged with PII.Sensitive - TableResourceTest tableResourceTest = new TableResourceTest(); - CreateTable tableReq = getSensitiveTableReq(test, tableResourceTest); - Table sensitiveTable = tableResourceTest.createAndCheckEntity(tableReq, ADMIN_AUTH_HEADERS); - String sensitiveColumnLink = - String.format("<#E::table::%s::columns::%s>", sensitiveTable.getFullyQualifiedName(), C1); - CreateTestCase create = - createRequest(test) - .withEntityLink(sensitiveColumnLink) - .withTestSuite(TEST_SUITE1.getFullyQualifiedName()) - .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) - .withParameterValues( - List.of( - new TestCaseParameterValue().withValue("100").withName("missingCountValue"))); - TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); - putTestCaseResult( - testCase.getFullyQualifiedName(), - new TestCaseResult() - .withResult("result") - .withTestCaseStatus(TestCaseStatus.Failed) - .withTimestamp(TestUtils.dateToTimestamp("2024-01-01")), - ADMIN_AUTH_HEADERS); - List columns = List.of(C1); - // Add 3 rows of sample data - List> rows = - Arrays.asList(List.of("c1Value1"), List.of("c1Value2"), List.of("c1Value3")); - // add sample data - putFailedRowsSample(testCase, columns, rows, ADMIN_AUTH_HEADERS); - // assert values are not masked for the table owner - TableData data = getSampleData(testCase.getId(), authHeaders(USER1.getName())); - assertFalse( - data.getRows().stream() - .flatMap(List::stream) - .map(r -> r == null ? "" : r) - .map(Object::toString) - .anyMatch(MASKED_VALUE::equals)); - // assert values are masked when is not the table owner - data = getSampleData(testCase.getId(), authHeaders(USER2.getName())); - assertEquals( - 3, - data.getRows().stream() - .flatMap(List::stream) - .map(r -> r == null ? "" : r) - .map(Object::toString) - .filter(MASKED_VALUE::equals) - .count()); - } - - @Test - void test_addInspectionQuery(TestInfo test) throws IOException { - CreateTestCase create = - createRequest(test) - .withEntityLink(TABLE_LINK) - .withTestSuite(TEST_SUITE1.getFullyQualifiedName()) - .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) - .withParameterValues( - List.of( - new TestCaseParameterValue().withValue("100").withName("missingCountValue"))); - TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); - String inspectionQuery = "SELECT * FROM test_table WHERE column1 = 'value1'"; - putInspectionQuery(testCase, inspectionQuery, ADMIN_AUTH_HEADERS); - TestCase updated = getTestCase(testCase.getFullyQualifiedName(), ADMIN_AUTH_HEADERS); - assertEquals(updated.getInspectionQuery(), inspectionQuery); - } - - @Test - @Execution(ExecutionMode.CONCURRENT) - protected void post_entityCreateWithInvalidName_400() { - // Create an entity with mandatory name field null - final CreateTestCase request = createRequest(null, "description", "displayName", null); - assertResponseContains( - () -> createEntity(request, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[name must not be null]"); - - // Create an entity with mandatory name field empty - final CreateTestCase request1 = createRequest("", "description", "displayName", null); - assertResponseContains( - () -> createEntity(request1, ADMIN_AUTH_HEADERS), - BAD_REQUEST, - TestUtils.getEntityNameLengthError(entityClass)); - - // Any entity name that has EntityLink separator must fail - final CreateTestCase request3 = - createRequest("invalid::Name", "description", "displayName", null); - assertResponseContains( - () -> createEntity(request3, ADMIN_AUTH_HEADERS), BAD_REQUEST, "name must match"); - } - - @Test - void createUpdate_DynamicAssertionTests(TestInfo testInfo) throws IOException { - CreateTestCase create = createRequest(testInfo).withUseDynamicAssertion(true); - TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); - testCase = getTestCase(testCase.getFullyQualifiedName(), ADMIN_AUTH_HEADERS); - assertTrue(testCase.getUseDynamicAssertion()); - CreateTestCase update = create.withUseDynamicAssertion(false); - updateEntity(update, OK, ADMIN_AUTH_HEADERS); - testCase = getTestCase(testCase.getFullyQualifiedName(), ADMIN_AUTH_HEADERS); - assertFalse(testCase.getUseDynamicAssertion()); - } - - private void putInspectionQuery(TestCase testCase, String sql, Map authHeaders) - throws IOException { - TestCase putResponse = putInspectionQuery(testCase.getId(), sql, authHeaders); + private void putInspectionQuery(TestCase testCase, String sql) throws IOException { + TestCase putResponse = putInspectionQuery(testCase.getId(), sql, ADMIN_AUTH_HEADERS); assertEquals(sql, putResponse.getInspectionQuery()); } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java index bbf560f0130..580c893bf78 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java @@ -98,8 +98,9 @@ public class EventSubscriptionResourceTest @Test void post_alertActionWithEnabledStateChange(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); LOG.info("creating webhook in disabled state"); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/" + endpoint; // Create a Disabled Generic Webhook CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName).withEnabled(false).withDestinations(getWebhook(uri)); @@ -108,7 +109,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(DISABLED, status.getStatus()); WebhookCallbackResource.EventDetails details = - webhookCallbackResource.getEventDetails(webhookName); + webhookCallbackResource.getEventDetails(endpoint); assertNull(details); // // Now enable the webhook @@ -143,7 +144,7 @@ public class EventSubscriptionResourceTest assertEquals(SubscriptionStatus.Status.ACTIVE, status2.getStatus()); // Ensure the call back notification has started - details = waitForFirstEvent(alert.getId(), webhookName, 25); + details = waitForFirstEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); SubscriptionStatus successDetails = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); @@ -454,7 +455,8 @@ public class EventSubscriptionResourceTest @Test public void post_createAndValidateEventSubscription(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/" + endpoint; CreateEventSubscription enabledWebhookRequest = new CreateEventSubscription() @@ -491,7 +493,8 @@ public class EventSubscriptionResourceTest @Test public void post_duplicateAlertsAreNotAllowed(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/" + endpoint; CreateEventSubscription genericWebhookRequest = new CreateEventSubscription() @@ -599,7 +602,8 @@ public class EventSubscriptionResourceTest @Test public void post_createAndValidateEventSubscription_SLACK(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription enabledWebhookRequest = new CreateEventSubscription() @@ -614,7 +618,7 @@ public class EventSubscriptionResourceTest EventSubscription alert = createEntity(enabledWebhookRequest, ADMIN_AUTH_HEADERS); waitForAllEventToComplete(alert.getId()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); ConcurrentLinkedQueue events = details.getEvents(); for (SlackMessage event : events) { validateSlackMessage(alert, event); @@ -675,8 +679,9 @@ public class EventSubscriptionResourceTest @Test void post_tableResource_filterByOwner_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); TableResourceTest tableResourceTest = new TableResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create the event subscription request with the Slack destination and table as the resource CreateEventSubscription genericWebhookActionRequest = @@ -700,7 +705,7 @@ public class EventSubscriptionResourceTest assertEquals(ACTIVE, status.getStatus()); // Verify no Slack events triggered initially - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); assertNull(details); // Create a table with a different owner (USER2), expect no alert @@ -709,14 +714,14 @@ public class EventSubscriptionResourceTest .createRequest(test.getClass().getName() + generateUniqueNumberAsString()) .withOwners(List.of(USER2.getEntityReference())); tableResourceTest.createEntity(createTable1, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertNull(details); // Create another table with the owner that matches the filter (USER_TEAM21), expect an alert CreateTable createTable = tableResourceTest.createRequest(test).withOwners(List.of(USER_TEAM21.getEntityReference())); Table table = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); // Clean up: delete the event subscription and created tables @@ -728,7 +733,8 @@ public class EventSubscriptionResourceTest void post_tableResource_filterByDomain_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); TableResourceTest tableResourceTest = new TableResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create the event subscription request with the Slack destination and table resource with // filterByDomain @@ -754,7 +760,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); assertNull(details); CreateDomain createDomain2 = domainResourceTest.createRequest("Engineering_2"); @@ -762,7 +768,7 @@ public class EventSubscriptionResourceTest CreateTable createTable = tableResourceTest.createRequest(test).withDomain(domainSecond.getName()); tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); // changeEvent on the table with correct domain will result in alerts assertNull(details); @@ -771,7 +777,7 @@ public class EventSubscriptionResourceTest .createRequest(test.getClass().getName() + "_secondTable") .withDomain(domain.getName()); tableResourceTest.createEntity(createTable2, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @@ -779,7 +785,8 @@ public class EventSubscriptionResourceTest void post_tableResource_fqnFilter_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); TableResourceTest tableResourceTest = new TableResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create the event subscription request with slack destination and table as the resource with // filterByFqn @@ -798,20 +805,20 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // alerts will trigger on the changeEvent of table resource assertNull(details); CreateTable createTable = tableResourceTest.createRequest(test.getClass().getName() + generateUniqueNumberAsString()); Table table1 = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); // alerts will trigger on the changeEvent of table resource with correct fqn assertNull(details); CreateTable createTable2 = tableResourceTest.createRequest(test).withName("test"); Table table2 = tableResourceTest.createEntity(createTable2, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); tableResourceTest.deleteEntity(table1.getId(), ADMIN_AUTH_HEADERS); @@ -821,7 +828,8 @@ public class EventSubscriptionResourceTest @Test void post_excluded_filters_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create the event subscription request with the Slack destination and table resource CreateEventSubscription genericWebhookActionRequest = @@ -841,7 +849,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); assertNull(details); TableResourceTest tableResourceTest = new TableResourceTest(); @@ -851,7 +859,7 @@ public class EventSubscriptionResourceTest tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); // Wait for the slack event and verify its details - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); // filters are EXCLUDED assertNull(details); @@ -862,7 +870,8 @@ public class EventSubscriptionResourceTest void post_tableResource_filterByOwner_AND_filterByDomain_Filter_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create the event subscription request with the Slack destination and table resource with // filterByOwner AND filterByDomain @@ -894,7 +903,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); assertNull(details); // wrong owner and correct domain -> no alerts @@ -906,7 +915,7 @@ public class EventSubscriptionResourceTest .withOwners(List.of(USER1.getEntityReference())) .withDomain(domain.getName()); tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertNull(details); // correct owner and wrong domain -> no alerts @@ -920,7 +929,7 @@ public class EventSubscriptionResourceTest .withOwners(List.of(USER1.getEntityReference())) .withDomain(domain2.getName()); tableResourceTest.createEntity(createTable2, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertNull(details); // correcr owner and correct domain -> alerts @@ -933,7 +942,7 @@ public class EventSubscriptionResourceTest Table table = tableResourceTest.createEntity(createTable3, ADMIN_AUTH_HEADERS); // Wait for the slack event and verify its details - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); tableResourceTest.deleteEntity(table.getId(), ADMIN_AUTH_HEADERS); } @@ -942,7 +951,8 @@ public class EventSubscriptionResourceTest void post_tableResource_noFilters_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); TableResourceTest tableResourceTest = new TableResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create the event subscription request with the Slack destination and table as the resource // without any filters @@ -955,7 +965,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // alerts will trigger on the changeEvent of table resource assertNull(details); @@ -964,14 +974,15 @@ public class EventSubscriptionResourceTest tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); // Wait for the slack event and verify its details - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @Test void post_topicResource_noFilters_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -981,7 +992,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by ChangeEvent occurrences related to resources as topic assertNull(details); @@ -993,15 +1004,16 @@ public class EventSubscriptionResourceTest .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @Test void post_topicResource_filterByFqn_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); TopicResourceTest topicResourceTest = new TopicResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -1020,7 +1032,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by changeEvent occurrences related to resources as topic with // filterByFqn @@ -1031,7 +1043,7 @@ public class EventSubscriptionResourceTest .createRequest(test) .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); // changeEvents associated with correct fqn will result in alerts assertNull(details); @@ -1041,15 +1053,16 @@ public class EventSubscriptionResourceTest .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest2, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @Test void post_topicResource_domain_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); TopicResourceTest topicResourceTest = new TopicResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -1072,7 +1085,7 @@ public class EventSubscriptionResourceTest EventSubscription alert = createAndCheckEntity(genericWebhookActionRequest, ADMIN_AUTH_HEADERS); SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by ChangeEvent occurrences related to resources as topic by // filterByDomain @@ -1085,15 +1098,16 @@ public class EventSubscriptionResourceTest .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @Test void post_topicResource_owner_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); TopicResourceTest topicResourceTest = new TopicResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -1110,7 +1124,7 @@ public class EventSubscriptionResourceTest EventSubscription alert = createAndCheckEntity(genericWebhookActionRequest, ADMIN_AUTH_HEADERS); SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by changeEvent occurrences related to resources as topic by // filerByOwner @@ -1123,7 +1137,7 @@ public class EventSubscriptionResourceTest .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @@ -1131,8 +1145,9 @@ public class EventSubscriptionResourceTest void post_topicResource_owner_AND_domain_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); DomainResourceTest domainResourceTest = new DomainResourceTest(); + String endpoint = test.getDisplayName(); TopicResourceTest topicResourceTest = new TopicResourceTest(); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -1160,7 +1175,7 @@ public class EventSubscriptionResourceTest EventSubscription alert = createAndCheckEntity(genericWebhookActionRequest, ADMIN_AUTH_HEADERS); SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by ChangeEvent occurrences related to resources as topic by // filterByDomain @@ -1175,7 +1190,7 @@ public class EventSubscriptionResourceTest .withDomain(domain.getName()) .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertNull(details); // correct owner AND wrong domain -> no alerts @@ -1190,7 +1205,7 @@ public class EventSubscriptionResourceTest .withDomain(domain2.getName()) .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest2, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertNull(details); // correct owner AND correct domain -> alerts @@ -1202,14 +1217,15 @@ public class EventSubscriptionResourceTest .withMessageSchema(TopicResourceTest.SCHEMA.withSchemaFields(TopicResourceTest.fields)); topicResourceTest.createEntity(topicRequest3, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @Test void post_ingestionPiplelineResource_noFilter_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -1220,7 +1236,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by ChangeEvent occurrences related to resources as // ingestionPipeline by domain filter @@ -1244,15 +1260,16 @@ public class EventSubscriptionResourceTest ingestionPipelineResourceTest.createEntity(request, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @Test void post_ingestionPiplelineResource_owner_alertAction(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); LOG.info("creating webhook in disabled state"); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; CreateEventSubscription genericWebhookActionRequest = createRequest(webhookName) @@ -1272,7 +1289,7 @@ public class EventSubscriptionResourceTest SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(ACTIVE, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); // Alerts are triggered only by ChangeEvent occurrences related to resources as // ingestionPipeline @@ -1296,7 +1313,7 @@ public class EventSubscriptionResourceTest ingestionPipelineResourceTest.createEntity(request, ADMIN_AUTH_HEADERS); - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); } @@ -1307,8 +1324,9 @@ public class EventSubscriptionResourceTest @Test void post_alertActionWithEnabledStateChange_SLACK(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); LOG.info("creating webhook in disabled state"); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint; // Create a Disabled Generic Webhook CreateEventSubscription genericWebhookActionRequest = @@ -1318,7 +1336,7 @@ public class EventSubscriptionResourceTest // For the DISABLED Publisher are not available, so it will have no status SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(DISABLED, status.getStatus()); - SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(webhookName); + SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint); assertNull(details); LOG.info("Enabling webhook Action"); @@ -1339,7 +1357,7 @@ public class EventSubscriptionResourceTest assertEquals(SubscriptionStatus.Status.ACTIVE, status2.getStatus()); // Ensure the call back notification has started - details = waitForFirstSlackEvent(alert.getId(), webhookName, 25); + details = waitForFirstSlackEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); ConcurrentLinkedQueue messages = details.getEvents(); for (SlackMessage sm : messages) { @@ -1451,7 +1469,8 @@ public class EventSubscriptionResourceTest @Test public void post_createAndValidateEventSubscription_MSTEAMS(TestInfo test) throws IOException { String webhookName = getEntityName(test); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/msteams/" + webhookName; + String endpoint = test.getDisplayName(); + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/msteams/" + endpoint; CreateEventSubscription enabledWebhookRequest = new CreateEventSubscription() @@ -1466,8 +1485,7 @@ public class EventSubscriptionResourceTest EventSubscription alert = createEntity(enabledWebhookRequest, ADMIN_AUTH_HEADERS); waitForAllEventToComplete(alert.getId()); - MSTeamsCallbackResource.EventDetails details = - teamsCallbackResource.getEventDetails(webhookName); + MSTeamsCallbackResource.EventDetails details = teamsCallbackResource.getEventDetails(endpoint); Awaitility.await() .pollInterval(Duration.ofMillis(100L)) @@ -1495,8 +1513,9 @@ public class EventSubscriptionResourceTest @Test void post_alertActionWithEnabledStateChange_MSTeams(TestInfo test) throws IOException { String webhookName = getEntityName(test); + String endpoint = test.getDisplayName(); LOG.info("creating webhook in disabled state"); - String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/msteams/" + webhookName; + String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/msteams/" + endpoint; // Create a Disabled Generic Webhook CreateEventSubscription genericWebhookActionRequest = @@ -1506,8 +1525,7 @@ public class EventSubscriptionResourceTest // For the DISABLED Publisher are not available, so it will have no status SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode()); assertEquals(DISABLED, status.getStatus()); - MSTeamsCallbackResource.EventDetails details = - teamsCallbackResource.getEventDetails(webhookName); + MSTeamsCallbackResource.EventDetails details = teamsCallbackResource.getEventDetails(endpoint); assertNull(details); LOG.info("Enabling webhook Action"); @@ -1528,7 +1546,7 @@ public class EventSubscriptionResourceTest assertEquals(SubscriptionStatus.Status.ACTIVE, status2.getStatus()); // Ensure the call back notification has started - details = waitForFirstMSTeamsEvent(alert.getId(), webhookName, 25); + details = waitForFirstMSTeamsEvent(alert.getId(), endpoint, 25); assertEquals(1, details.getEvents().size()); ConcurrentLinkedQueue messages = details.getEvents(); for (TeamsMessage teamsMessage : messages) { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/FeedResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/FeedResourceTest.java index 0b1c15056ee..3f8c92ca05d 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/FeedResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/FeedResourceTest.java @@ -219,7 +219,8 @@ public class FeedResourceTest extends OpenMetadataApplicationTest { // Create thread without addressed to entity in the request CreateThread create = create().withFrom(USER.getName()).withAbout("<>"); // Invalid EntityLink - String failureReason = "[about must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\"]"; + String failureReason = + "[about must match \"(?U)^<#E::\\w+::(?:[^:<>|]|:[^:<>|])+(?:::(?:[^:<>|]|:[^:<>|])+)*>$\"]"; assertResponseContains( () -> createThread(create, USER_AUTH_HEADERS), BAD_REQUEST, failureReason); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java index f35d6810e38..a3375627643 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java @@ -153,7 +153,7 @@ public class SuggestionsResourceTest extends OpenMetadataApplicationTest { CreateSuggestion create = create().withEntityLink("<>"); // Invalid EntityLink String failureReason = - "[entityLink must match \"(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$\"]"; + "[entityLink must match \"(?U)^<#E::\\w+::(?:[^:<>|]|:[^:<>|])+(?:::(?:[^:<>|]|:[^:<>|])+)*>$\"]"; assertResponseContains( () -> createSuggestion(create, USER_AUTH_HEADERS), BAD_REQUEST, failureReason); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/util/EntityUtilTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/util/EntityUtilTest.java index 15413aca948..74d51041ef4 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/util/EntityUtilTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/util/EntityUtilTest.java @@ -2,9 +2,12 @@ package org.openmetadata.service.util; import static org.junit.jupiter.api.Assertions.*; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; import org.openmetadata.schema.entity.data.GlossaryTerm; import org.openmetadata.schema.entity.data.Table; +import org.openmetadata.service.resources.feeds.MessageParser; class EntityUtilTest { @Test @@ -15,4 +18,167 @@ class EntityUtilTest { EntityUtil.isDescriptionRequired( GlossaryTerm.class)); // GlossaryTerm entity requires description } + + @Test + void test_entityLinkParser() { + + // Valid entity links + Map expected = new HashMap<>(); + expected.put("entityLink", "<#E::table::users>"); + expected.put("arrayFieldName", null); + expected.put("arrayFieldValue", null); + expected.put("fieldName", null); + expected.put("entityFQN", "users"); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY"); + expected.put("fullyQualifiedFieldType", "table"); + expected.put("fullyQualifiedFieldValue", "users"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::table::users.foo.\"bar.baz\">"); + expected.put("arrayFieldName", null); + expected.put("arrayFieldValue", null); + expected.put("fieldName", null); + expected.put("entityFQN", "users.foo.\"bar.baz\""); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY"); + expected.put("fullyQualifiedFieldType", "table"); + expected.put("fullyQualifiedFieldValue", "users.foo.\"bar.baz\""); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::db::customers>"); + expected.put("arrayFieldName", null); + expected.put("arrayFieldValue", null); + expected.put("fieldName", null); + expected.put("entityFQN", "customers"); + expected.put("entityType", "db"); + expected.put("linkType", "ENTITY"); + expected.put("fullyQualifiedFieldType", "db"); + expected.put("fullyQualifiedFieldValue", "customers"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::table::users::column::id>"); + expected.put("arrayFieldName", "id"); + expected.put("arrayFieldValue", null); + expected.put("fieldName", "column"); + expected.put("entityFQN", "users"); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY_ARRAY_FIELD"); + expected.put("fullyQualifiedFieldType", "table.column.member"); + expected.put("fullyQualifiedFieldValue", "users.id"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::table::orders::column::status::type::enum>"); + expected.put("arrayFieldName", "status"); + expected.put("arrayFieldValue", "type::enum"); + expected.put("fieldName", "column"); + expected.put("entityFQN", "orders"); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY_ARRAY_FIELD"); + expected.put("fullyQualifiedFieldType", "table.column.member"); + expected.put("fullyQualifiedFieldValue", "orders.status.type::enum"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::db::schema::table::view::column>"); + expected.put("arrayFieldName", "view"); + expected.put("arrayFieldValue", "column"); + expected.put("fieldName", "table"); + expected.put("entityFQN", "schema"); + expected.put("entityType", "db"); + expected.put("linkType", "ENTITY_ARRAY_FIELD"); + expected.put("fullyQualifiedFieldType", "db.table.member"); + expected.put("fullyQualifiedFieldValue", "schema.view.column"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::table::foo@bar>"); + expected.put("arrayFieldName", null); + expected.put("arrayFieldValue", null); + expected.put("fieldName", null); + expected.put("entityFQN", "foo@bar"); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY"); + expected.put("fullyQualifiedFieldType", "table"); + expected.put("fullyQualifiedFieldValue", "foo@bar"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::table::foo[bar]>"); + expected.put("arrayFieldName", null); + expected.put("arrayFieldValue", null); + expected.put("fieldName", null); + expected.put("entityFQN", "foo[bar]"); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY"); + expected.put("fullyQualifiedFieldType", "table"); + expected.put("fullyQualifiedFieldValue", "foo[bar]"); + verifyEntityLinkParser(expected); + + expected.clear(); + expected.put("entityLink", "<#E::table::special!@#$%^&*()_+[]{};:\\'\",./?>"); + expected.put("arrayFieldName", null); + expected.put("arrayFieldValue", null); + expected.put("fieldName", null); + expected.put("entityFQN", "special!@#$%^&*()_+[]{};:\\'\",./?"); + expected.put("entityType", "table"); + expected.put("linkType", "ENTITY"); + expected.put("fullyQualifiedFieldType", "table"); + expected.put("fullyQualifiedFieldValue", "special!@#$%^&*()_+[]{};:\\'\",./?"); + verifyEntityLinkParser(expected); + + // Invalid entity link + expected.clear(); + expected.put("entityLink", "<#E::table::special!@#$%^&*()_+[]{}|;:\\'\",./?>"); + // EntityLink with `|` character will not be parsed correctly and everything after `|` will be + // ignored + org.opentest4j.AssertionFailedError exception = + assertThrows( + org.opentest4j.AssertionFailedError.class, () -> verifyEntityLinkParser(expected)); + assertEquals( + "expected: <<#E::table::special!@#$%^&*()_+[]{}|;:\\'\",./?>> but was: <<#E::table::special!@#$%^&*()_+[]{}>>", + exception.getMessage()); + + expected.clear(); + expected.put("entityLink", "<#E::table::user::column>"); + IllegalArgumentException argException = + assertThrows(IllegalArgumentException.class, () -> verifyEntityLinkParser(expected)); + assertEquals( + "Entity link was not found in <#E::table::user::column>", argException.getMessage()); + + expected.clear(); + expected.put("entityLink", "<#E::table::user>name::column>"); + exception = + assertThrows( + org.opentest4j.AssertionFailedError.class, () -> verifyEntityLinkParser(expected)); + assertEquals( + "expected: <<#E::table::user>name::column>> but was: <<#E::table::user>>", + exception.getMessage()); + + expected.clear(); + expected.put("entityLink", "<#E::table::foo<>bar::baz>"); + argException = + assertThrows(IllegalArgumentException.class, () -> verifyEntityLinkParser(expected)); + assertEquals( + "Entity link was not found in <#E::table::foo<>bar::baz>", argException.getMessage()); + } + + void verifyEntityLinkParser(Map expected) { + MessageParser.EntityLink entityLink = + MessageParser.EntityLink.parse(expected.get("entityLink")); + assertEquals(expected.get("entityLink"), entityLink.getLinkString()); + assertEquals(expected.get("arrayFieldName"), entityLink.getArrayFieldName()); + assertEquals(expected.get("arrayFieldValue"), entityLink.getArrayFieldValue()); + assertEquals(expected.get("entityType"), entityLink.getEntityType()); + assertEquals(expected.get("fieldName"), entityLink.getFieldName()); + assertEquals(expected.get("entityFQN"), entityLink.getEntityFQN()); + assertEquals(expected.get("linkType"), entityLink.getLinkType().toString()); + assertEquals(expected.get("fullyQualifiedFieldType"), entityLink.getFullyQualifiedFieldType()); + assertEquals( + expected.get("fullyQualifiedFieldValue"), entityLink.getFullyQualifiedFieldValue()); + } } diff --git a/openmetadata-spec/src/main/resources/json/schema/type/basic.json b/openmetadata-spec/src/main/resources/json/schema/type/basic.json index 4c2528391da..314954800e0 100644 --- a/openmetadata-spec/src/main/resources/json/schema/type/basic.json +++ b/openmetadata-spec/src/main/resources/json/schema/type/basic.json @@ -113,7 +113,7 @@ "entityLink": { "description": "Link to an entity or field within an entity using this format `<#E::{entities}::{entityType}::{field}::{arrayFieldName}::{arrayFieldValue}`.", "type": "string", - "pattern": "(?U)^<#E::\\w+::[\\w'\\- .&/:+\"\\\\()$#%]+>$" + "pattern": "(?U)^<#E::\\w+::(?:[^:<>|]|:[^:<>|])+(?:::(?:[^:<>|]|:[^:<>|])+)*>$" }, "entityName": { "description": "Name that identifies an entity.", diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx index 93af7ea200e..632e4819ba4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx @@ -23,6 +23,7 @@ import { FormItemLayout, } from '../../../../interface/FormUtils.interface'; import { generateFormFields } from '../../../../utils/formUtils'; +import { escapeESReservedCharacters } from '../../../../utils/StringsUtils'; import { AddTestCaseList } from '../../AddTestCaseList/AddTestCaseList.component'; import { AddTestSuitePipelineProps } from '../AddDataQualityTest.interface'; import './add-test-suite-pipeline.style.less'; @@ -142,7 +143,9 @@ const AddTestSuitePipeline = ({ ]} valuePropName="selectedTest">