MINOR - Domain Independent DP Rule (#23067)

* MINOR - Domain Independent DP Rule

* handle DP

* Handle DP

* add migration

* improve rule mgmt

* improve rule mgmt

* add test for bulk op

* fix test

* handle in bulk

---------

Co-authored-by: sonika-shah <58761340+sonika-shah@users.noreply.github.com>
This commit is contained in:
Pere Miquel Brull 2025-08-29 17:28:29 +02:00 committed by GitHub
parent 4f6ba7b010
commit abcdc4e3d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 907 additions and 106 deletions

View File

@ -0,0 +1,19 @@
-- Add "Data Product Domain Validation" rule to existing entityRulesSettings configuration
UPDATE openmetadata_settings
SET json = JSON_ARRAY_APPEND(
json,
'$.entitySemantics',
JSON_OBJECT(
'name', 'Data Product Domain Validation',
'description', 'Validates that Data Products assigned to an entity match the entity''s domains.',
'rule', '{"validateDataProductDomainMatch":[{"var":"dataProducts"},{"var":"domains"}]}',
'enabled', true,
'provider', 'system'
)
)
WHERE configType = 'entityRulesSettings'
AND JSON_EXTRACT(json, '$.entitySemantics') IS NOT NULL
AND NOT JSON_CONTAINS(
JSON_EXTRACT(json, '$.entitySemantics[*].name'),
JSON_QUOTE('Data Product Domain Validation')
);

View File

@ -0,0 +1,21 @@
-- Add "Data Product Domain Validation" rule to existing entityRulesSettings configuration
UPDATE openmetadata_settings
SET json = jsonb_set(
json,
'{entitySemantics}',
(json->'entitySemantics') || jsonb_build_object(
'name', 'Data Product Domain Validation',
'description', 'Validates that Data Products assigned to an entity match the entity''s domains.',
'rule', '{"validateDataProductDomainMatch":[{"var":"dataProducts"},{"var":"domains"}]}',
'enabled', true,
'provider', 'system'
)::jsonb,
true
)
WHERE configtype = 'entityRulesSettings'
AND json->'entitySemantics' IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM jsonb_array_elements(json->'entitySemantics') AS rule
WHERE rule->>'name' = 'Data Product Domain Validation'
);

View File

@ -201,7 +201,14 @@ class OMetaDomainTest(TestCase):
def test_add_remove_assets_to_data_product(self):
"""We can add assets to a data product"""
self.metadata.create_or_update(data=self.create_domain)
domain: Domain = self.metadata.create_or_update(data=self.create_domain)
domains_ref = EntityReferenceList(
root=[EntityReference(id=domain.id, type="domain")]
)
# Make sure the dashboard belongs to the data product domain!
self.metadata.patch_domain(
entity=Dashboard, source=self.dashboard, domains=domains_ref
)
data_product: DataProduct = self.metadata.create_or_update(
data=self.create_data_product
)

View File

@ -31,7 +31,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.EntityInterface;
@ -50,6 +49,8 @@ import org.openmetadata.schema.type.change.ChangeSource;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.domains.DataProductResource;
import org.openmetadata.service.rules.RuleEngine;
import org.openmetadata.service.rules.RuleValidationException;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.LineageUtil;
@ -214,34 +215,73 @@ public class DataProductRepository extends EntityRepository<DataProduct> {
BulkOperationResult result =
new BulkOperationResult().withStatus(ApiStatus.SUCCESS).withDryRun(false);
List<BulkResponse> success = new ArrayList<>();
List<BulkResponse> failed = new ArrayList<>();
EntityUtil.populateEntityReferences(request.getAssets());
for (EntityReference ref : request.getAssets()) {
result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1);
// Get the data product reference for validation
DataProduct dataProduct = find(entityId, ALL);
EntityReference dataProductRef = dataProduct.getEntityReference();
removeCrossDomainDataProducts(ref, relationship);
if (isAdd) {
addRelationship(entityId, ref.getId(), fromEntity, ref.getType(), relationship);
} else {
deleteRelationship(entityId, fromEntity, ref.getId(), ref.getType(), relationship);
}
success.add(new BulkResponse().withRequest(ref));
result.setNumberOfRowsPassed(result.getNumberOfRowsPassed() + 1);
searchRepository.updateEntity(ref);
// Fetch all asset entities in bulk for validation
List<EntityInterface> assetEntities = new ArrayList<>();
if (isAdd && !request.getAssets().isEmpty()) {
assetEntities = Entity.getEntities(request.getAssets(), "domains,dataProducts", ALL);
}
result.withSuccessRequest(success);
for (int i = 0; i < request.getAssets().size(); i++) {
EntityReference ref = request.getAssets().get(i);
result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1);
// Create a Change Event on successful addition/removal of assets
if (result.getStatus().equals(ApiStatus.SUCCESS)) {
try {
if (isAdd) {
EntityInterface assetEntity = assetEntities.get(i);
validateAssetDataProductAssignment(assetEntity, dataProductRef);
addRelationship(entityId, ref.getId(), fromEntity, ref.getType(), relationship);
} else {
deleteRelationship(entityId, fromEntity, ref.getId(), ref.getType(), relationship);
}
success.add(new BulkResponse().withRequest(ref));
result.setNumberOfRowsPassed(result.getNumberOfRowsPassed() + 1);
searchRepository.updateEntity(ref);
} catch (RuleValidationException e) {
LOG.warn(
"Validation failed for asset {} in bulk operation: {}", ref.getId(), e.getMessage());
failed.add(new BulkResponse().withRequest(ref).withMessage(e.getMessage()));
result.setNumberOfRowsFailed(result.getNumberOfRowsFailed() + 1);
result.setStatus(ApiStatus.PARTIAL_SUCCESS);
} catch (Exception e) {
LOG.error(
"Unexpected error during bulk operation for asset {}: {}",
ref.getId(),
e.getMessage(),
e);
failed.add(
new BulkResponse().withRequest(ref).withMessage("Internal error: " + e.getMessage()));
result.setNumberOfRowsFailed(result.getNumberOfRowsFailed() + 1);
result.setStatus(ApiStatus.PARTIAL_SUCCESS);
}
}
result.withSuccessRequest(success).withFailedRequest(failed);
// If all operations failed, mark as failure
if (success.isEmpty() && !failed.isEmpty()) {
result.setStatus(ApiStatus.FAILURE);
}
// Create a Change Event on successful operations
if (!success.isEmpty()) {
EntityInterface entityInterface = Entity.getEntity(fromEntity, entityId, "id", ALL);
List<EntityReference> successfulAssets = new ArrayList<>();
for (BulkResponse response : success) {
successfulAssets.add((EntityReference) response.getRequest());
}
ChangeDescription change =
addBulkAddRemoveChangeDescription(
entityInterface.getVersion(), isAdd, request.getAssets(), null);
entityInterface.getVersion(), isAdd, successfulAssets, null);
ChangeEvent changeEvent =
getChangeEvent(entityInterface, change, fromEntity, entityInterface.getVersion());
Entity.getCollectionDAO().changeEventDAO().insert(JsonUtils.pojoToJson(changeEvent));
@ -250,50 +290,39 @@ public class DataProductRepository extends EntityRepository<DataProduct> {
return result;
}
private void removeCrossDomainDataProducts(EntityReference ref, Relationship relationship) {
EntityReference domain =
getFromEntityRef(ref.getId(), ref.getType(), relationship, DOMAIN, false);
List<EntityReference> dataProducts = getDataProducts(ref.getId(), ref.getType());
/**
* Validates that an asset can be assigned to a data product according to configured rules.
* This method leverages the RuleEngine to validate domain matching rules that are enabled.
*
* @param assetEntity The asset entity interface (pre-fetched with domains,dataProducts)
* @param dataProductRef The data product entity reference
* @throws RuleValidationException if validation fails
*/
private void validateAssetDataProductAssignment(
EntityInterface assetEntity, EntityReference dataProductRef) {
try {
if (!dataProducts.isEmpty() && domain != null) {
// Map dataProduct -> domain
Map<UUID, UUID> associatedDomains =
daoCollection
.relationshipDAO()
.findFromBatch(
dataProducts.stream()
.map(dp -> dp.getId().toString())
.collect(Collectors.toList()),
relationship.ordinal(),
DOMAIN)
.stream()
.collect(
Collectors.toMap(
rec -> UUID.fromString(rec.getToId()),
rec -> UUID.fromString(rec.getFromId())));
List<EntityReference> currentDataProducts = listOrEmpty(assetEntity.getDataProducts());
List<EntityReference> updatedDataProducts = new ArrayList<>(currentDataProducts);
updatedDataProducts.add(dataProductRef);
List<EntityReference> dataProductsToDelete =
dataProducts.stream()
.filter(
dataProduct -> {
UUID associatedDomainId = associatedDomains.get(dataProduct.getId());
return associatedDomainId != null && !associatedDomainId.equals(domain.getId());
})
.collect(Collectors.toList());
assetEntity.setDataProducts(updatedDataProducts);
RuleEngine.getInstance().evaluate(assetEntity, true, false);
if (!dataProductsToDelete.isEmpty()) {
daoCollection
.relationshipDAO()
.bulkRemoveFromRelationship(
dataProductsToDelete.stream()
.map(EntityReference::getId)
.collect(Collectors.toList()),
ref.getId(),
DATA_PRODUCT,
ref.getType(),
relationship.ordinal());
LineageUtil.removeDataProductsLineage(ref.getId(), ref.getType(), dataProductsToDelete);
}
} catch (RuleValidationException e) {
// Re-throw validation exceptions with context about the bulk operation
throw new RuleValidationException(
String.format(
"Cannot assign asset '%s' (type: %s) to data product '%s': %s",
assetEntity.getName(),
assetEntity.getEntityReference().getType(),
dataProductRef.getName(),
e.getMessage()));
} catch (Exception e) {
LOG.warn(
"Error during asset data product validation for asset {}: {}",
assetEntity.getId(),
e.getMessage());
}
}

View File

@ -2567,38 +2567,6 @@ public abstract class EntityRepository<T extends EntityInterface> {
return RestUtil.getHref(uriInfo, collectionPath, id);
}
private void removeCrossDomainDataProducts(List<EntityReference> removedDomains, T entity) {
if (!supportsDataProducts) {
return;
}
List<EntityReference> entityDataProducts = entity.getDataProducts();
if (entityDataProducts == null || nullOrEmpty(removedDomains)) {
// If no domains are being removed, nothing to do
return;
}
for (EntityReference domain : removedDomains) {
// Fetch domain data products
List<UUID> domainDataProductIds =
daoCollection
.relationshipDAO()
.findToIds(domain.getId(), DOMAIN, Relationship.HAS.ordinal(), Entity.DATA_PRODUCT);
entityDataProducts.removeIf(
dataProduct -> {
boolean isNotDomainDataProduct = !domainDataProductIds.contains(dataProduct.getId());
if (isNotDomainDataProduct) {
LOG.info(
"Removing data product {} from entity {}",
dataProduct.getFullyQualifiedName(),
entity.getEntityReference().getType());
}
return isNotDomainDataProduct;
});
}
}
@Transaction
public final PutResponse<T> restoreEntity(String updatedBy, UUID id) {
// If an entity being restored contains other **deleted** children entities, restore them
@ -4103,8 +4071,6 @@ public abstract class EntityRepository<T extends EntityInterface> {
entityReferenceMatch)) {
updateDomains(original, origDomains, updatedDomains);
updated.setDomains(updatedDomains);
// Clean up data products associated with the domains we are removing
removeCrossDomainDataProducts(removedDomains, updated);
} else {
updated.setDomains(original.getDomains());
}
@ -4169,8 +4135,6 @@ public abstract class EntityRepository<T extends EntityInterface> {
entityReferenceMatch)) {
updateDomains(original, origDomains, updatedDomains);
updated.setDomains(updatedDomains);
// Clean up data products associated with the domains we are removing
removeCrossDomainDataProducts(removedDomains, updated);
} else {
updated.setDomains(original.getDomains());
}
@ -4207,7 +4171,6 @@ public abstract class EntityRepository<T extends EntityInterface> {
updatedDataProducts,
true,
entityReferenceListMatch)) {
removeCrossDomainDataProducts(removedDomains, updated);
updatedDataProducts = listOrEmpty(updated.getDataProducts());
}
updateFromRelationships(

View File

@ -11,11 +11,18 @@ import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException;
import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator;
import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.entity.domains.DataProduct;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.Entity;
public class LogicOps {
@ -107,6 +114,60 @@ public class LogicOps {
return true;
});
// Domain validation for Data Product assignment
// This validates that entities being assigned to Data Products have matching domains
jsonLogic.addOperation(
"validateDataProductDomainMatch",
(args) -> {
if (nullOrEmpty(args) || args.length < 2) {
return true; // If no data products or domains, validation passes
}
List<EntityReference> dataProducts =
JsonUtils.convertValue(args[0], new TypeReference<List<EntityReference>>() {});
List<EntityReference> domains =
JsonUtils.convertValue(args[1], new TypeReference<List<EntityReference>>() {});
if (nullOrEmpty(dataProducts)) {
return true; // If no data products, validation passes
} else if (nullOrEmpty(domains)) {
return false; // If data products but no domains, validation fails
}
// Convert entity domains to a Set of UUIDs for efficient lookup
Set<UUID> entityDomainIds =
domains.stream().map(EntityReference::getId).collect(Collectors.toSet());
// Get all data product entities in bulk instead of using a loop
try {
List<DataProduct> dpEntities =
Entity.getEntities(dataProducts, "domains", Include.NON_DELETED);
for (DataProduct dpEntity : dpEntities) {
List<EntityReference> dpDomains = dpEntity.getDomains();
if (nullOrEmpty(dpDomains)) {
continue; // If data product has no domains, skip validation
}
// Convert data product domains to a Set of UUIDs for efficient comparison
Set<UUID> dpDomainIds =
dpDomains.stream().map(EntityReference::getId).collect(Collectors.toSet());
// Use Collections.disjoint for O(n+m) performance instead of O(n*m)
boolean hasMatchingDomain = !Collections.disjoint(entityDomainIds, dpDomainIds);
if (!hasMatchingDomain) {
return false; // Domain mismatch found
}
}
} catch (Exception e) {
// If we can't fetch the data products, fail validation for safety
return false;
}
return true; // All data products have matching domains
});
// Example: {"filterReferenceByType":[{"var":"owner"},"team"]}
jsonLogic.addOperation(
"filterReferenceByType",

View File

@ -12,6 +12,10 @@ public class RuleValidationException extends IllegalArgumentException {
super(formatMessage(rule, message));
}
public RuleValidationException(String message) {
super(message);
}
public RuleValidationException(List<SemanticsRule> rules, String message) {
super(formatMessage(rules, message));
}

View File

@ -29,6 +29,13 @@
"enabled": false,
"entityType": "table",
"provider": "system"
},
{
"name": "Data Product Domain Validation",
"description": "Validates that Data Products assigned to an entity match the entity's domains.",
"rule": "{\"validateDataProductDomainMatch\":[{\"var\":\"dataProducts\"},{\"var\":\"domains\"}]}",
"enabled": true,
"provider": "system"
}
]
}

View File

@ -292,9 +292,6 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
public static final String ENTITY_LINK_MATCH_ERROR =
"[entityLink must match \"(?U)^<#E::\\w+::(?:[^:<>|]|:[^:<>|])+(?:::(?:[^:<>|]|:[^:<>|])+)*>$\"]";
public static final String MULTIDOMAIN_RULE_ERROR =
"Rule [Multiple Domains are not allowed] validation failed: Entity does not satisfy the rule. Rule context: "
+ "By default, we only allow entities to be assigned to a single domain, except for Users and Teams.";
// Random unicode string generator to test entity name accepts all the unicode characters
protected static final RandomStringGenerator RANDOM_STRING_GENERATOR =
@ -681,6 +678,16 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
throws IOException;
public static void toggleMultiDomainSupport(Boolean enable) {
toggleRule(MULTI_DOMAIN_RULE, enable);
}
/**
* Generic method to toggle any rule by name in the system settings.
*
* @param ruleName The name of the rule to toggle
* @param enable The desired enabled state of the rule
*/
public static void toggleRule(String ruleName, Boolean enable) {
SystemRepository systemRepository = Entity.getSystemRepository();
Settings currentSettings =
@ -691,7 +698,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
.getEntitySemantics()
.forEach(
rule -> {
if (MULTI_DOMAIN_RULE.equals(rule.getName())) {
if (ruleName.equals(rule.getName())) {
rule.setEnabled(enable);
}
});
@ -1203,8 +1210,8 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
assertResponse(
() -> patchEntityAndCheck(entity, originalJson, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change),
NOT_FOUND,
String.format("dataProduct instance for %s not found", dataProductReference.getId()));
BAD_REQUEST,
"Rule [Data Product Domain Validation] validation failed: Entity does not satisfy the rule. Rule context: Validates that Data Products assigned to an entity match the entity's domains.");
}
@Test

View File

@ -27,8 +27,11 @@ import org.openmetadata.schema.entity.data.Topic;
import org.openmetadata.schema.entity.domains.DataProduct;
import org.openmetadata.schema.entity.domains.Domain;
import org.openmetadata.schema.entity.type.Style;
import org.openmetadata.schema.type.ApiStatus;
import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.api.BulkAssets;
import org.openmetadata.schema.type.api.BulkOperationResult;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.EntityNotFoundException;
@ -340,4 +343,170 @@ public class DataProductResourceTest extends EntityResourceTest<DataProduct, Cre
assertCommonFieldChange(fieldName, expected, actual);
}
}
@Test
void testBulkAddAssets_DataProductDomainValidation(TestInfo test) throws IOException {
// Create domains for testing
DomainResourceTest domainResourceTest = new DomainResourceTest();
Domain dataDomain =
domainResourceTest.createEntity(
domainResourceTest
.createRequest(test.getDisplayName() + "_DataDomain")
.withName("DataDomain")
.withDescription("Data domain for testing"),
ADMIN_AUTH_HEADERS);
Domain engineeringDomain =
domainResourceTest.createEntity(
domainResourceTest
.createRequest(test.getDisplayName() + "_EngineeringDomain")
.withName("EngineeringDomain")
.withDescription("Engineering domain for testing"),
ADMIN_AUTH_HEADERS);
// Create data product with data domain
CreateDataProduct createDataProduct =
createRequest(test.getDisplayName() + "_DataProduct")
.withDomains(List.of(dataDomain.getFullyQualifiedName()));
DataProduct dataProduct = createEntity(createDataProduct, ADMIN_AUTH_HEADERS);
// Create tables with different domain assignments
TableResourceTest tableResourceTest = new TableResourceTest();
// Table with matching domain (should succeed in bulk add)
org.openmetadata.schema.entity.data.Table matchingTable =
tableResourceTest.createEntity(
tableResourceTest
.createRequest(test.getDisplayName() + "_MatchingTable")
.withDomains(List.of(dataDomain.getFullyQualifiedName())),
ADMIN_AUTH_HEADERS);
// Table with non-matching domain (should fail in bulk add)
org.openmetadata.schema.entity.data.Table nonMatchingTable =
tableResourceTest.createEntity(
tableResourceTest
.createRequest(test.getDisplayName() + "_NonMatchingTable")
.withDomains(List.of(engineeringDomain.getFullyQualifiedName())),
ADMIN_AUTH_HEADERS);
// Test 1: Bulk add assets with matching domains - should succeed
BulkAssets matchingAssetsRequest =
new BulkAssets().withAssets(List.of(matchingTable.getEntityReference()));
BulkOperationResult successResult =
TestUtils.put(
getCollection().path("/" + dataProduct.getName() + "/assets/add"),
matchingAssetsRequest,
BulkOperationResult.class,
Status.OK,
ADMIN_AUTH_HEADERS);
assertEquals(ApiStatus.SUCCESS, successResult.getStatus());
assertEquals(1, successResult.getNumberOfRowsProcessed());
assertEquals(1, successResult.getNumberOfRowsPassed());
assertEquals(0, successResult.getNumberOfRowsFailed());
assertEquals(1, successResult.getSuccessRequest().size());
assertTrue(successResult.getFailedRequest().isEmpty());
// Test 2: Bulk add assets with non-matching domains - should fail
BulkAssets nonMatchingAssetsRequest =
new BulkAssets().withAssets(List.of(nonMatchingTable.getEntityReference()));
BulkOperationResult failResult =
TestUtils.put(
getCollection().path("/" + dataProduct.getName() + "/assets/add"),
nonMatchingAssetsRequest,
BulkOperationResult.class,
Status.OK,
ADMIN_AUTH_HEADERS);
assertEquals(ApiStatus.FAILURE, failResult.getStatus());
assertEquals(1, failResult.getNumberOfRowsProcessed());
assertEquals(0, failResult.getNumberOfRowsPassed());
assertEquals(1, failResult.getNumberOfRowsFailed());
assertTrue(failResult.getSuccessRequest().isEmpty());
assertEquals(1, failResult.getFailedRequest().size());
assertTrue(failResult.getFailedRequest().get(0).getMessage().contains("Cannot assign asset"));
assertTrue(
failResult
.getFailedRequest()
.get(0)
.getMessage()
.contains("Data Product Domain Validation"));
// Test 3: Mixed bulk operation - one matching, one non-matching
BulkAssets mixedAssetsRequest =
new BulkAssets()
.withAssets(
List.of(matchingTable.getEntityReference(), nonMatchingTable.getEntityReference()));
// Remove the matching table first to test mixed scenario cleanly
BulkAssets removeMatchingRequest =
new BulkAssets().withAssets(List.of(matchingTable.getEntityReference()));
TestUtils.put(
getCollection().path("/" + dataProduct.getName() + "/assets/remove"),
removeMatchingRequest,
BulkOperationResult.class,
Status.OK,
ADMIN_AUTH_HEADERS);
// Now test mixed operation
BulkOperationResult mixedResult =
TestUtils.put(
getCollection().path("/" + dataProduct.getName() + "/assets/add"),
mixedAssetsRequest,
BulkOperationResult.class,
Status.OK,
ADMIN_AUTH_HEADERS);
assertEquals(ApiStatus.PARTIAL_SUCCESS, mixedResult.getStatus());
assertEquals(2, mixedResult.getNumberOfRowsProcessed());
assertEquals(1, mixedResult.getNumberOfRowsPassed()); // matching table succeeds
assertEquals(1, mixedResult.getNumberOfRowsFailed()); // non-matching table fails
assertEquals(1, mixedResult.getSuccessRequest().size());
assertEquals(1, mixedResult.getFailedRequest().size());
// Test 4: Disable rule and retry failed operation - should succeed
String originalRuleName = "Data Product Domain Validation";
EntityResourceTest.toggleRule(originalRuleName, false);
try {
BulkOperationResult disabledRuleResult =
TestUtils.put(
getCollection().path("/" + dataProduct.getName() + "/assets/add"),
nonMatchingAssetsRequest,
BulkOperationResult.class,
Status.OK,
ADMIN_AUTH_HEADERS);
assertEquals(ApiStatus.SUCCESS, disabledRuleResult.getStatus());
assertEquals(1, disabledRuleResult.getNumberOfRowsProcessed());
assertEquals(1, disabledRuleResult.getNumberOfRowsPassed());
assertEquals(0, disabledRuleResult.getNumberOfRowsFailed());
} finally {
// Re-enable rule for other tests
EntityResourceTest.toggleRule(originalRuleName, true);
}
// Test 5: Bulk remove assets - should always work regardless of domain validation
BulkAssets removeAllRequest =
new BulkAssets()
.withAssets(
List.of(matchingTable.getEntityReference(), nonMatchingTable.getEntityReference()));
BulkOperationResult removeResult =
TestUtils.put(
getCollection().path("/" + dataProduct.getName() + "/assets/remove"),
removeAllRequest,
BulkOperationResult.class,
Status.OK,
ADMIN_AUTH_HEADERS);
assertEquals(ApiStatus.SUCCESS, removeResult.getStatus());
assertEquals(2, removeResult.getNumberOfRowsProcessed());
assertEquals(2, removeResult.getNumberOfRowsPassed());
assertEquals(0, removeResult.getNumberOfRowsFailed());
}
}

View File

@ -24,10 +24,12 @@ import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.openmetadata.schema.api.data.CreateDataContract;
import org.openmetadata.schema.api.data.CreateTable;
import org.openmetadata.schema.api.domains.CreateDataProduct;
import org.openmetadata.schema.api.domains.CreateDomain;
import org.openmetadata.schema.configuration.EntityRulesSettings;
import org.openmetadata.schema.entity.data.DataContract;
import org.openmetadata.schema.entity.data.Table;
import org.openmetadata.schema.entity.domains.DataProduct;
import org.openmetadata.schema.entity.domains.Domain;
import org.openmetadata.schema.settings.SettingsType;
import org.openmetadata.schema.type.Column;
@ -38,8 +40,10 @@ import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationTest;
import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.data.DataContractResourceTest;
import org.openmetadata.service.resources.databases.TableResourceTest;
import org.openmetadata.service.resources.domains.DataProductResourceTest;
import org.openmetadata.service.resources.domains.DomainResourceTest;
import org.openmetadata.service.resources.settings.SettingsCache;
@ -48,6 +52,7 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
private static TableResourceTest tableResourceTest;
private static DataContractResourceTest dataContractResourceTest;
private static DomainResourceTest domainResourceTest;
private static DataProductResourceTest dataProductResourceTest;
private static final String C1 = "id";
private static final String C2 = "name";
@ -58,6 +63,10 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
private static final String OWNERSHIP_RULE_EXC =
"Rule [Multiple Users or Single Team Ownership] validation failed: Entity does not satisfy the rule. "
+ "Rule context: Validates that an entity has either multiple owners or a single team as the owner.";
private static final String DATA_PRODUCT_DOMAIN_RULE_EXC =
"Rule [Data Product Domain Validation] validation failed: Entity does not satisfy the rule. "
+ "Rule context: Validates that Data Products assigned to an entity match the entity's domains.";
private static final String DATA_PRODUCT_DOMAIN_RULE = "Data Product Domain Validation";
@BeforeAll
static void setup(TestInfo test) throws IOException, URISyntaxException {
@ -65,6 +74,7 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
tableResourceTest.setup(test);
dataContractResourceTest = new DataContractResourceTest();
domainResourceTest = new DomainResourceTest();
dataProductResourceTest = new DataProductResourceTest();
}
Table getMockTable(TestInfo test) {
@ -202,6 +212,8 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
() ->
new IllegalStateException(
"No glossary validation rule found for tables. Review the entityRulesSettings.json file."));
// Enable it for the test
glossaryRule.setEnabled(true);
// No glossary terms, should pass
RuleEngine.getInstance().evaluate(table, List.of(glossaryRule), false, false);
@ -314,7 +326,7 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
CreateDomain createDomain =
domainResourceTest
.createRequest(test)
.createRequest(test.getDisplayName() + "_Data")
.withName("Data")
.withDescription("Data domain")
.withOwners(List.of(USER1_REF));
@ -352,7 +364,7 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
dataContractResourceTest.createDataContract(createContractForTable);
// Table does indeed blow up
// Table does indeed blow up when evaluated against data contract rules
assertThrows(
RuleValidationException.class,
() -> RuleEngine.getInstance().evaluate(tableWithContract, false, true));
@ -463,4 +475,506 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
table.withTags(List.of(TIER1_TAG_LABEL));
RuleEngine.getInstance().evaluate(table, List.of(piiRule), false, false);
}
/**
* Helper method to create a real Domain entity for testing
*/
Domain createTestDomain(String name, TestInfo test) throws IOException {
CreateDomain createDomain =
domainResourceTest
.createRequest(test.getDisplayName() + "_" + name)
.withDescription("Test domain: " + name);
return domainResourceTest.createEntity(createDomain, ADMIN_AUTH_HEADERS);
}
/**
* Helper method to create a real DataProduct entity for testing
*/
DataProduct createTestDataProduct(String name, List<Domain> domains, TestInfo test)
throws IOException {
String entityName = test.getDisplayName() + "_" + name;
CreateDataProduct createDataProduct =
dataProductResourceTest
.createRequest(entityName)
.withName(entityName)
.withDescription("Test data product: " + name);
if (domains != null && !domains.isEmpty()) {
createDataProduct.withDomains(
domains.stream().map(domain -> domain.getFullyQualifiedName()).toList());
}
return dataProductResourceTest.createEntity(createDataProduct, ADMIN_AUTH_HEADERS);
}
/**
* Helper method to get the Data Product Domain Validation rule
*/
SemanticsRule getDataProductDomainRule() {
List<SemanticsRule> rules =
SettingsCache.getSetting(SettingsType.ENTITY_RULES_SETTINGS, EntityRulesSettings.class)
.getEntitySemantics();
return rules.stream()
.filter(rule -> DATA_PRODUCT_DOMAIN_RULE.equals(rule.getName()))
.findFirst()
.orElseThrow(
() ->
new IllegalStateException(
"Data Product Domain Validation rule not found. Review the entityRulesSettings.json file."));
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_RuleEnabled_MatchingDomains_ShouldPass(TestInfo test)
throws IOException {
// Create domains
Domain dataDomain = createTestDomain("Data", test);
Domain engineeringDomain = createTestDomain("Engineering", test);
// Create data products with matching domains
DataProduct dataProduct1 = createTestDataProduct("Product1", List.of(dataDomain), test);
DataProduct dataProduct2 = createTestDataProduct("Product2", List.of(engineeringDomain), test);
EntityResourceTest.toggleMultiDomainSupport(false);
// Create table with domains that match the data products
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(
List.of(
dataDomain.getFullyQualifiedName(), engineeringDomain.getFullyQualifiedName()))
.withDataProducts(
List.of(
dataProduct1.getFullyQualifiedName(), dataProduct2.getFullyQualifiedName()));
Table table = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
SemanticsRule rule = getDataProductDomainRule();
// Should pass validation as entity domains match data product domains
RuleEngine.getInstance().evaluate(table, List.of(rule), false, false);
EntityResourceTest.toggleMultiDomainSupport(true);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_RuleEnabled_NoMatchingDomains_ShouldFail(TestInfo test)
throws IOException {
// Create domains
Domain dataDomain = createTestDomain("Data", test);
Domain engineeringDomain = createTestDomain("Engineering", test);
Domain marketingDomain = createTestDomain("Marketing", test);
// Create data products with different domains than entity
DataProduct dataProduct1 = createTestDataProduct("Product1", List.of(dataDomain), test);
DataProduct dataProduct2 = createTestDataProduct("Product2", List.of(engineeringDomain), test);
// Create table with domains that don't match the data products
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(List.of(marketingDomain.getFullyQualifiedName()))
.withDataProducts(
List.of(
dataProduct1.getFullyQualifiedName(), dataProduct2.getFullyQualifiedName()));
// Should fail validation as entity domains don't match data product domains
assertResponse(
() -> tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
// But I can disable the rule and create it
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, false);
tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, true);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_RuleEnabled_MixedMatching_ShouldFail(TestInfo test)
throws IOException {
// Create domains
Domain dataDomain = createTestDomain("Data", test);
Domain engineeringDomain = createTestDomain("Engineering", test);
Domain marketingDomain = createTestDomain("Marketing", test);
// Create data products - one matches, one doesn't
DataProduct matchingDataProduct =
createTestDataProduct("MatchingProduct", List.of(dataDomain), test);
DataProduct nonMatchingDataProduct =
createTestDataProduct("NonMatchingProduct", List.of(marketingDomain), test);
// Create table with some matching and some non-matching domains
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(
List.of(
dataDomain.getFullyQualifiedName(), engineeringDomain.getFullyQualifiedName()))
.withDataProducts(
List.of(
matchingDataProduct.getFullyQualifiedName(),
nonMatchingDataProduct.getFullyQualifiedName()));
// Should fail during entity creation due to rule validation
assertResponse(
() -> tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
"Entity does not satisfy multiple rules\n"
+ "Rule [Multiple Domains are not allowed] validation failed: Rule context: By default, we only allow entities to be assigned to a single domain, except for Users and Teams.\n"
+ "Rule [Data Product Domain Validation] validation failed: Rule context: Validates that Data Products assigned to an entity match the entity's domains.");
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_RuleDisabled_DifferentDomains_ShouldPass(TestInfo test)
throws IOException {
// Store original rule state
SemanticsRule originalRule = getDataProductDomainRule();
Boolean originalEnabled = originalRule.getEnabled();
try {
// Disable the rule using the system settings
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, false);
// Create domains
Domain dataDomain = createTestDomain("Data", test);
Domain engineeringDomain = createTestDomain("Engineering", test);
Domain marketingDomain = createTestDomain("Marketing", test);
// Create data products with completely different domains than entity
DataProduct dataProduct1 = createTestDataProduct("Product1", List.of(dataDomain), test);
DataProduct dataProduct2 =
createTestDataProduct("Product2", List.of(engineeringDomain), test);
// Create table with domains that don't match the data products
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(List.of(marketingDomain.getFullyQualifiedName()))
.withDataProducts(
List.of(
dataProduct1.getFullyQualifiedName(), dataProduct2.getFullyQualifiedName()));
Table table = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
SemanticsRule rule = getDataProductDomainRule();
// Should pass validation when rule is disabled, even with non-matching domains
RuleEngine.getInstance().evaluate(table, List.of(rule), false, false);
} finally {
// Restore original rule state
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, originalEnabled);
}
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_EdgeCase_EntityWithNullDomains_ShouldHandleGracefully(
TestInfo test) throws IOException {
// Create domain and data product
Domain dataDomain = createTestDomain("Data", test);
DataProduct dataProduct = createTestDataProduct("Product1", List.of(dataDomain), test);
// Create table with no domains but assigned to data products with domains
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(null) // No domains
.withDataProducts(List.of(dataProduct.getFullyQualifiedName()));
// Should fail during entity creation due to rule validation
assertResponse(
() -> tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_EdgeCase_DataProductWithNullDomains_ShouldHandleGracefully(
TestInfo test) throws IOException {
// Create domain for entity and data product with no domains
Domain dataDomain = createTestDomain("Data", test);
DataProduct dataProduct = createTestDataProduct("Product1", null, test);
// Create table with domains but assigned to data products with no domains
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(List.of(dataProduct.getFullyQualifiedName()));
// Should fail during entity creation due to rule validation
assertResponse(
() -> tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_EdgeCase_NoDataProducts_ShouldAlwaysPass(TestInfo test)
throws IOException {
// Create domain for entity
Domain dataDomain = createTestDomain("Data", test);
// Create table with domains but no data products assigned
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(null); // No data products
Table table = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
SemanticsRule rule = getDataProductDomainRule();
// Should always pass validation when entity has no data products
RuleEngine.getInstance().evaluate(table, List.of(rule), false, false);
// Also test with empty list
CreateTable createTableEmpty =
tableResourceTest
.createRequest(test.getDisplayName() + "_empty")
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(List.of()); // Empty data products
Table tableEmpty = tableResourceTest.createEntity(createTableEmpty, ADMIN_AUTH_HEADERS);
RuleEngine.getInstance().evaluate(tableEmpty, List.of(rule), false, false);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_EdgeCase_NoDomains_ShouldHandleGracefully(TestInfo test)
throws IOException {
// Create data product with no domains
DataProduct dataProduct = createTestDataProduct("Product1", null, test);
// Create table with no domains assigned to data products with no domains
CreateTable createTable =
tableResourceTest
.createRequest(test)
.withDomains(null) // No domains
.withDataProducts(List.of(dataProduct.getFullyQualifiedName()));
// Should fail during entity creation due to rule validation
assertResponse(
() -> tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
// Also test with empty domain lists
CreateTable createTableEmpty =
tableResourceTest
.createRequest(test.getDisplayName() + "_empty")
.withDomains(List.of()) // Empty domains
.withDataProducts(List.of(dataProduct.getFullyQualifiedName()));
// Should also fail during entity creation due to rule validation
assertResponse(
() -> tableResourceTest.createEntity(createTableEmpty, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_ComprehensiveScenarios(TestInfo test) throws IOException {
// Store original rule state for cleanup
SemanticsRule originalRule = getDataProductDomainRule();
Boolean originalEnabled = originalRule.getEnabled();
try {
// Setup test data - domains and data products
Domain dataDomain = createTestDomain("Data", test);
Domain engineeringDomain = createTestDomain("Engineering", test);
Domain marketingDomain = createTestDomain("Marketing", test);
DataProduct dataProductWithDataDomain =
createTestDataProduct("DataProduct", List.of(dataDomain), test);
DataProduct dataProductWithEngineeringDomain =
createTestDataProduct("EngineeringProduct", List.of(engineeringDomain), test);
DataProduct dataProductWithNoDomains = createTestDataProduct("NoDomainProduct", null, test);
// Test 1: Matching domains - should pass
CreateTable matchingDomainsTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_matching")
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(List.of(dataProductWithDataDomain.getFullyQualifiedName()));
Table createdTable = tableResourceTest.createEntity(matchingDomainsTable, ADMIN_AUTH_HEADERS);
assertNotNull(createdTable);
assertEquals(1, createdTable.getDomains().size());
assertEquals(1, createdTable.getDataProducts().size());
// Test 2: Non-matching domains - should fail
CreateTable nonMatchingDomainsTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_non_matching")
.withDomains(List.of(marketingDomain.getFullyQualifiedName()))
.withDataProducts(List.of(dataProductWithDataDomain.getFullyQualifiedName()));
assertResponse(
() -> tableResourceTest.createEntity(nonMatchingDomainsTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
// Test 3: Disable rule and retry - should pass
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, false);
Table createdWithDisabledRule =
tableResourceTest.createEntity(nonMatchingDomainsTable, ADMIN_AUTH_HEADERS);
assertNotNull(createdWithDisabledRule);
// Re-enable rule for remaining tests
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, true);
// Test 4: Entity with null domains but data products with domains - should fail
CreateTable nullDomainsTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_null_domains")
.withDomains(null)
.withDataProducts(List.of(dataProductWithDataDomain.getFullyQualifiedName()));
assertResponse(
() -> tableResourceTest.createEntity(nullDomainsTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
// Test 5: Entity with domains but data products with null domains - should fail
CreateTable dataProductNullDomainsTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_dp_null_domains")
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(List.of(dataProductWithNoDomains.getFullyQualifiedName()));
assertResponse(
() -> tableResourceTest.createEntity(dataProductNullDomainsTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
// Test 6: No data products - should always pass
CreateTable noDataProductsTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_no_data_products")
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(null);
Table tableWithNoDataProducts =
tableResourceTest.createEntity(noDataProductsTable, ADMIN_AUTH_HEADERS);
assertNotNull(tableWithNoDataProducts);
// Test 7: Empty data products list - should also pass
CreateTable emptyDataProductsTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_empty_data_products")
.withDomains(List.of(dataDomain.getFullyQualifiedName()))
.withDataProducts(List.of());
Table tableWithEmptyDataProducts =
tableResourceTest.createEntity(emptyDataProductsTable, ADMIN_AUTH_HEADERS);
assertNotNull(tableWithEmptyDataProducts);
// Test 8: Disable rule for failing case and verify success
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, false);
CreateTable previouslyFailingTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_previously_failing")
.withDomains(null)
.withDataProducts(List.of(dataProductWithDataDomain.getFullyQualifiedName()));
Table successWithDisabledRule =
tableResourceTest.createEntity(previouslyFailingTable, ADMIN_AUTH_HEADERS);
assertNotNull(successWithDisabledRule);
} finally {
// Restore original states
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, originalEnabled);
}
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testDataProductDomainValidation_MultiDomainScenarios(TestInfo test) throws IOException {
// Store original states
SemanticsRule originalRule = getDataProductDomainRule();
Boolean originalEnabled = originalRule.getEnabled();
EntityResourceTest.toggleMultiDomainSupport(true);
try {
// Setup test data
Domain dataDomain = createTestDomain("Data", test);
Domain engineeringDomain = createTestDomain("Engineering", test);
Domain marketingDomain = createTestDomain("Marketing", test);
DataProduct matchingDataProduct =
createTestDataProduct("MatchingProduct", List.of(dataDomain), test);
DataProduct nonMatchingDataProduct =
createTestDataProduct("NonMatchingProduct", List.of(marketingDomain), test);
// Test 1: Multiple domains with mixed matching data products - should fail
// First disable the multi-domain rule to test only the data product domain rule
EntityResourceTest.toggleRule("Multiple Domains are not allowed", false);
CreateTable mixedMatchingTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_mixed")
.withDomains(
List.of(
dataDomain.getFullyQualifiedName(),
engineeringDomain.getFullyQualifiedName()))
.withDataProducts(
List.of(
matchingDataProduct.getFullyQualifiedName(),
nonMatchingDataProduct.getFullyQualifiedName()));
assertResponse(
() -> tableResourceTest.createEntity(mixedMatchingTable, ADMIN_AUTH_HEADERS),
Response.Status.BAD_REQUEST,
DATA_PRODUCT_DOMAIN_RULE_EXC);
// Test 2: Disable rule and retry - should pass
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, false);
Table createdWithDisabledRule =
tableResourceTest.createEntity(mixedMatchingTable, ADMIN_AUTH_HEADERS);
assertNotNull(createdWithDisabledRule);
assertEquals(2, createdWithDisabledRule.getDomains().size());
assertEquals(2, createdWithDisabledRule.getDataProducts().size());
// Test 3: Re-enable rule and test with properly matching domains
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, true);
DataProduct anotherMatchingProduct =
createTestDataProduct("AnotherMatchingProduct", List.of(engineeringDomain), test);
CreateTable properlyMatchingTable =
tableResourceTest
.createRequest(test.getDisplayName() + "_properly_matching")
.withDomains(
List.of(
dataDomain.getFullyQualifiedName(),
engineeringDomain.getFullyQualifiedName()))
.withDataProducts(
List.of(
matchingDataProduct.getFullyQualifiedName(),
anotherMatchingProduct.getFullyQualifiedName()));
Table properlyCreatedTable =
tableResourceTest.createEntity(properlyMatchingTable, ADMIN_AUTH_HEADERS);
assertNotNull(properlyCreatedTable);
assertEquals(2, properlyCreatedTable.getDomains().size());
assertEquals(2, properlyCreatedTable.getDataProducts().size());
} finally {
// Restore original states
EntityResourceTest.toggleRule(DATA_PRODUCT_DOMAIN_RULE, originalEnabled);
EntityResourceTest.toggleRule("Multiple Domains are not allowed", true);
EntityResourceTest.toggleMultiDomainSupport(false);
}
}
}