mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-13 01:38:13 +00:00
* Implement Fetch DQ Test Cases by FollowedBy * Update generated TypeScript types * Fix wrong conflict resolution * Refactored into Optional --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
49bdf1a112
commit
a630ca2be6
@ -1491,6 +1491,8 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
|||||||
|
|
||||||
entity.setIncrementalChangeDescription(change);
|
entity.setIncrementalChangeDescription(change);
|
||||||
entity.setChangeDescription(change);
|
entity.setChangeDescription(change);
|
||||||
|
// Populate followers before postUpdate to ensure propagation to children
|
||||||
|
entity.setFollowers(getFollowers(entity));
|
||||||
postUpdate(entity, entity);
|
postUpdate(entity, entity);
|
||||||
return new PutResponse<>(Status.OK, changeEvent, ENTITY_FIELDS_CHANGED);
|
return new PutResponse<>(Status.OK, changeEvent, ENTITY_FIELDS_CHANGED);
|
||||||
}
|
}
|
||||||
@ -1966,6 +1968,8 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
|||||||
|
|
||||||
entity.setChangeDescription(change);
|
entity.setChangeDescription(change);
|
||||||
entity.setIncrementalChangeDescription(change);
|
entity.setIncrementalChangeDescription(change);
|
||||||
|
// Populate followers before postUpdate to ensure propagation to children
|
||||||
|
entity.setFollowers(getFollowers(entity));
|
||||||
postUpdate(entity, entity);
|
postUpdate(entity, entity);
|
||||||
return new PutResponse<>(Status.OK, changeEvent, ENTITY_FIELDS_CHANGED);
|
return new PutResponse<>(Status.OK, changeEvent, ENTITY_FIELDS_CHANGED);
|
||||||
}
|
}
|
||||||
@ -3060,6 +3064,16 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final void inheritFollowers(T entity, Fields fields, EntityInterface parent) {
|
||||||
|
if (fields.contains(FIELD_FOLLOWERS) && nullOrEmpty(entity.getFollowers()) && parent != null) {
|
||||||
|
entity.setFollowers(
|
||||||
|
Optional.ofNullable(parent.getFollowers())
|
||||||
|
.filter(list -> !list.isEmpty())
|
||||||
|
.orElse(Collections.emptyList()));
|
||||||
|
listOrEmpty(entity.getFollowers()).forEach(follower -> follower.setInherited(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final void inheritOwners(T entity, Fields fields, EntityInterface parent) {
|
public final void inheritOwners(T entity, Fields fields, EntityInterface parent) {
|
||||||
if (fields.contains(FIELD_OWNERS) && nullOrEmpty(entity.getOwners()) && parent != null) {
|
if (fields.contains(FIELD_OWNERS) && nullOrEmpty(entity.getOwners()) && parent != null) {
|
||||||
entity.setOwners(getInheritedOwners(entity, fields, parent));
|
entity.setOwners(getInheritedOwners(entity, fields, parent));
|
||||||
|
@ -366,12 +366,15 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
|
|||||||
// Inherit from the table/column
|
// Inherit from the table/column
|
||||||
EntityInterface tableOrColumn =
|
EntityInterface tableOrColumn =
|
||||||
Entity.getEntity(
|
Entity.getEntity(
|
||||||
EntityLink.parse(testCase.getEntityLink()), "owners,domains,tags,columns", ALL);
|
EntityLink.parse(testCase.getEntityLink()),
|
||||||
|
"owners,domains,tags,columns,followers",
|
||||||
|
ALL);
|
||||||
if (tableOrColumn != null) {
|
if (tableOrColumn != null) {
|
||||||
inheritOwners(testCase, fields, tableOrColumn);
|
inheritOwners(testCase, fields, tableOrColumn);
|
||||||
inheritDomains(testCase, fields, tableOrColumn);
|
inheritDomains(testCase, fields, tableOrColumn);
|
||||||
if (tableOrColumn instanceof Table) {
|
if (tableOrColumn instanceof Table) {
|
||||||
inheritTags(testCase, fields, (Table) tableOrColumn);
|
inheritTags(testCase, fields, (Table) tableOrColumn);
|
||||||
|
inheritFollowers(testCase, fields, (Table) tableOrColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,9 +95,9 @@ public class TestCaseResource extends EntityResource<TestCase, TestCaseRepositor
|
|||||||
private final TestCaseMapper mapper = new TestCaseMapper();
|
private final TestCaseMapper mapper = new TestCaseMapper();
|
||||||
private final TestCaseResultMapper testCaseResultMapper = new TestCaseResultMapper();
|
private final TestCaseResultMapper testCaseResultMapper = new TestCaseResultMapper();
|
||||||
static final String FIELDS =
|
static final String FIELDS =
|
||||||
"owners,reviewers,entityStatus,testSuite,testDefinition,testSuites,incidentId,domains,tags";
|
"owners,reviewers,entityStatus,testSuite,testDefinition,testSuites,incidentId,domains,tags,followers";
|
||||||
static final String SEARCH_FIELDS_EXCLUDE =
|
static final String SEARCH_FIELDS_EXCLUDE =
|
||||||
"testPlatforms,table,database,databaseSchema,service,testSuite,dataQualityDimension,testCaseType,originEntityFQN";
|
"testPlatforms,table,database,databaseSchema,service,testSuite,dataQualityDimension,testCaseType,originEntityFQN,followers";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TestCase addHref(UriInfo uriInfo, TestCase test) {
|
public TestCase addHref(UriInfo uriInfo, TestCase test) {
|
||||||
@ -416,81 +416,52 @@ public class TestCaseResource extends EntityResource<TestCase, TestCaseRepositor
|
|||||||
description = "Filter test cases by the user who created them",
|
description = "Filter test cases by the user who created them",
|
||||||
schema = @Schema(type = "string"))
|
schema = @Schema(type = "string"))
|
||||||
@QueryParam("createdBy")
|
@QueryParam("createdBy")
|
||||||
String createdBy)
|
String createdBy,
|
||||||
|
@Parameter(
|
||||||
|
description = "Filter test cases by entities followed by a user",
|
||||||
|
schema = @Schema(type = "string"))
|
||||||
|
@QueryParam("followedBy")
|
||||||
|
String followedBy)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if ((startTimestamp == null && endTimestamp != null)
|
// Validate parameters
|
||||||
|| (startTimestamp != null && endTimestamp == null)) {
|
validateTimestamps(startTimestamp, endTimestamp);
|
||||||
throw new IllegalArgumentException("startTimestamp and endTimestamp must be used together");
|
|
||||||
}
|
// Build search filters
|
||||||
SearchSortFilter searchSortFilter =
|
SearchSortFilter searchSortFilter =
|
||||||
new SearchSortFilter(sortField, sortType, sortNestedPath, sortNestedMode);
|
new SearchSortFilter(sortField, sortType, sortNestedPath, sortNestedMode);
|
||||||
SearchListFilter searchListFilter = new SearchListFilter(include);
|
SearchListFilter searchListFilter =
|
||||||
searchListFilter.addQueryParam("testSuiteId", testSuiteId);
|
buildSearchListFilter(
|
||||||
searchListFilter.addQueryParam("includeAllTests", includeAllTests.toString());
|
include,
|
||||||
searchListFilter.addQueryParam("testCaseStatus", status);
|
testSuiteId,
|
||||||
searchListFilter.addQueryParam("testCaseType", type);
|
includeAllTests,
|
||||||
searchListFilter.addQueryParam("testPlatforms", testPlatforms);
|
status,
|
||||||
searchListFilter.addQueryParam("dataQualityDimension", dataQualityDimension);
|
type,
|
||||||
searchListFilter.addQueryParam("q", q);
|
testPlatforms,
|
||||||
searchListFilter.addQueryParam("excludeFields", SEARCH_FIELDS_EXCLUDE);
|
dataQualityDimension,
|
||||||
searchListFilter.addQueryParam("includeFields", includeFields);
|
|
||||||
searchListFilter.addQueryParam("domains", domain);
|
|
||||||
searchListFilter.addQueryParam("tags", tags);
|
|
||||||
searchListFilter.addQueryParam("tier", tier);
|
|
||||||
searchListFilter.addQueryParam("serviceName", serviceName);
|
|
||||||
searchListFilter.addQueryParam("createdBy", createdBy);
|
|
||||||
if (!nullOrEmpty(owner)) {
|
|
||||||
EntityInterface entity;
|
|
||||||
StringBuilder owners = new StringBuilder();
|
|
||||||
try {
|
|
||||||
User user = Entity.getEntityByName(Entity.USER, owner, "teams", ALL);
|
|
||||||
owners.append(user.getId().toString());
|
|
||||||
if (!nullOrEmpty(user.getTeams())) {
|
|
||||||
owners
|
|
||||||
.append(",")
|
|
||||||
.append(
|
|
||||||
user.getTeams().stream()
|
|
||||||
.map(t -> t.getId().toString())
|
|
||||||
.collect(Collectors.joining(",")));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// If the owner is not a user, then we'll try to get team
|
|
||||||
entity = Entity.getEntityByName(Entity.TEAM, owner, "", ALL);
|
|
||||||
owners.append(entity.getId().toString());
|
|
||||||
}
|
|
||||||
searchListFilter.addQueryParam("owners", owners.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startTimestamp != null) {
|
|
||||||
if (startTimestamp > endTimestamp) {
|
|
||||||
throw new IllegalArgumentException("startTimestamp must be less than endTimestamp");
|
|
||||||
}
|
|
||||||
searchListFilter.addQueryParam("startTimestamp", startTimestamp.toString());
|
|
||||||
searchListFilter.addQueryParam("endTimestamp", endTimestamp.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
ResourceContextInterface resourceContextInterface =
|
|
||||||
getResourceContext(entityLink, searchListFilter);
|
|
||||||
// Override OperationContext to change the entity to table and operation from VIEW_ALL to
|
|
||||||
// VIEW_TESTS
|
|
||||||
OperationContext operationContext =
|
|
||||||
new OperationContext(Entity.TABLE, MetadataOperation.VIEW_TESTS);
|
|
||||||
Fields fields = getFields(fieldsParam);
|
|
||||||
|
|
||||||
ResultList<TestCase> tests =
|
|
||||||
super.listInternalFromSearch(
|
|
||||||
uriInfo,
|
|
||||||
securityContext,
|
|
||||||
fields,
|
|
||||||
searchListFilter,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
searchSortFilter,
|
|
||||||
q,
|
q,
|
||||||
queryString,
|
includeFields,
|
||||||
operationContext,
|
domain,
|
||||||
resourceContextInterface);
|
tags,
|
||||||
return PIIMasker.getTestCases(tests, authorizer, securityContext);
|
tier,
|
||||||
|
serviceName,
|
||||||
|
createdBy,
|
||||||
|
owner,
|
||||||
|
followedBy,
|
||||||
|
startTimestamp,
|
||||||
|
endTimestamp);
|
||||||
|
|
||||||
|
// Execute search
|
||||||
|
return executeTestCaseSearch(
|
||||||
|
uriInfo,
|
||||||
|
securityContext,
|
||||||
|
fieldsParam,
|
||||||
|
entityLink,
|
||||||
|
searchListFilter,
|
||||||
|
searchSortFilter,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
q,
|
||||||
|
queryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -1157,4 +1128,140 @@ public class TestCaseResource extends EntityResource<TestCase, TestCaseRepositor
|
|||||||
}
|
}
|
||||||
return resourceContext;
|
return resourceContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String resolveUserOrTeamIds(String userOrTeamName, boolean includeTeamMembers) {
|
||||||
|
if (nullOrEmpty(userOrTeamName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder ids = new StringBuilder();
|
||||||
|
try {
|
||||||
|
// Try to resolve as a user first
|
||||||
|
User user =
|
||||||
|
Entity.getEntityByName(
|
||||||
|
Entity.USER, userOrTeamName, includeTeamMembers ? "teams" : "", ALL);
|
||||||
|
ids.append(user.getId().toString());
|
||||||
|
|
||||||
|
// If includeTeamMembers is true and user has teams, add team IDs
|
||||||
|
if (includeTeamMembers && !nullOrEmpty(user.getTeams())) {
|
||||||
|
ids.append(",")
|
||||||
|
.append(
|
||||||
|
user.getTeams().stream()
|
||||||
|
.map(t -> t.getId().toString())
|
||||||
|
.collect(Collectors.joining(",")));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If not a user, try to resolve as a team
|
||||||
|
EntityInterface entity = Entity.getEntityByName(Entity.TEAM, userOrTeamName, "", ALL);
|
||||||
|
ids.append(entity.getId().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateTimestamps(Long startTimestamp, Long endTimestamp) {
|
||||||
|
if ((startTimestamp == null && endTimestamp != null)
|
||||||
|
|| (startTimestamp != null && endTimestamp == null)) {
|
||||||
|
throw new IllegalArgumentException("startTimestamp and endTimestamp must be used together");
|
||||||
|
}
|
||||||
|
if (startTimestamp != null && startTimestamp > endTimestamp) {
|
||||||
|
throw new IllegalArgumentException("startTimestamp must be less than endTimestamp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SearchListFilter buildSearchListFilter(
|
||||||
|
Include include,
|
||||||
|
String testSuiteId,
|
||||||
|
Boolean includeAllTests,
|
||||||
|
String status,
|
||||||
|
String type,
|
||||||
|
String testPlatforms,
|
||||||
|
String dataQualityDimension,
|
||||||
|
String q,
|
||||||
|
String includeFields,
|
||||||
|
String domain,
|
||||||
|
String tags,
|
||||||
|
String tier,
|
||||||
|
String serviceName,
|
||||||
|
String createdBy,
|
||||||
|
String owner,
|
||||||
|
String followedBy,
|
||||||
|
Long startTimestamp,
|
||||||
|
Long endTimestamp) {
|
||||||
|
|
||||||
|
SearchListFilter searchListFilter = new SearchListFilter(include);
|
||||||
|
|
||||||
|
// Add basic parameters
|
||||||
|
searchListFilter.addQueryParam("testSuiteId", testSuiteId);
|
||||||
|
searchListFilter.addQueryParam("includeAllTests", includeAllTests.toString());
|
||||||
|
searchListFilter.addQueryParam("testCaseStatus", status);
|
||||||
|
searchListFilter.addQueryParam("testCaseType", type);
|
||||||
|
searchListFilter.addQueryParam("testPlatforms", testPlatforms);
|
||||||
|
searchListFilter.addQueryParam("dataQualityDimension", dataQualityDimension);
|
||||||
|
searchListFilter.addQueryParam("q", q);
|
||||||
|
searchListFilter.addQueryParam("excludeFields", SEARCH_FIELDS_EXCLUDE);
|
||||||
|
searchListFilter.addQueryParam("includeFields", includeFields);
|
||||||
|
searchListFilter.addQueryParam("domains", domain);
|
||||||
|
searchListFilter.addQueryParam("tags", tags);
|
||||||
|
searchListFilter.addQueryParam("tier", tier);
|
||||||
|
searchListFilter.addQueryParam("serviceName", serviceName);
|
||||||
|
searchListFilter.addQueryParam("createdBy", createdBy);
|
||||||
|
|
||||||
|
// Handle owner and followedBy parameters
|
||||||
|
if (!nullOrEmpty(owner)) {
|
||||||
|
String ownerIds = resolveUserOrTeamIds(owner, true); // include team members
|
||||||
|
searchListFilter.addQueryParam("owners", ownerIds);
|
||||||
|
}
|
||||||
|
if (!nullOrEmpty(followedBy)) {
|
||||||
|
String followerIds = resolveUserOrTeamIds(followedBy, false); // don't include team members
|
||||||
|
searchListFilter.addQueryParam("followedBy", followerIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp parameters
|
||||||
|
if (startTimestamp != null) {
|
||||||
|
searchListFilter.addQueryParam("startTimestamp", startTimestamp.toString());
|
||||||
|
searchListFilter.addQueryParam("endTimestamp", endTimestamp.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchListFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResultList<TestCase> executeTestCaseSearch(
|
||||||
|
UriInfo uriInfo,
|
||||||
|
SecurityContext securityContext,
|
||||||
|
String fieldsParam,
|
||||||
|
String entityLink,
|
||||||
|
SearchListFilter searchListFilter,
|
||||||
|
SearchSortFilter searchSortFilter,
|
||||||
|
int limit,
|
||||||
|
int offset,
|
||||||
|
String q,
|
||||||
|
String queryString)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
ResourceContextInterface resourceContextInterface =
|
||||||
|
getResourceContext(entityLink, searchListFilter);
|
||||||
|
|
||||||
|
// Override OperationContext to change the entity to table and operation from VIEW_ALL to
|
||||||
|
// VIEW_TESTS
|
||||||
|
OperationContext operationContext =
|
||||||
|
new OperationContext(Entity.TABLE, MetadataOperation.VIEW_TESTS);
|
||||||
|
Fields fields = getFields(fieldsParam);
|
||||||
|
|
||||||
|
ResultList<TestCase> tests =
|
||||||
|
super.listInternalFromSearch(
|
||||||
|
uriInfo,
|
||||||
|
securityContext,
|
||||||
|
fields,
|
||||||
|
searchListFilter,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchSortFilter,
|
||||||
|
q,
|
||||||
|
queryString,
|
||||||
|
operationContext,
|
||||||
|
resourceContextInterface);
|
||||||
|
|
||||||
|
return PIIMasker.getTestCases(tests, authorizer, securityContext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,6 +231,53 @@ public interface SearchClient<T> {
|
|||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
// Script for propagating followers to TestCases from their parent tables.
|
||||||
|
// TestCases can only have inherited followers (no direct followers allowed),
|
||||||
|
// so we always replace the entire followers list when propagating.
|
||||||
|
// Followers are stored as UUID strings in the search index for efficiency.
|
||||||
|
// This script only applies to TestCases - does nothing for other entity types.
|
||||||
|
String ADD_FOLLOWERS_SCRIPT =
|
||||||
|
"""
|
||||||
|
if (ctx._source.containsKey('entityType') && ctx._source.entityType == 'testCase') {
|
||||||
|
// TestCases can only have inherited followers - always replace
|
||||||
|
if (params.containsKey('updatedFollowers') && params.updatedFollowers != null) {
|
||||||
|
List followerIds = new ArrayList();
|
||||||
|
for (def follower : params.updatedFollowers) {
|
||||||
|
if (follower != null && follower.containsKey('id')) {
|
||||||
|
followerIds.add(follower.id.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx._source.followers = followerIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do nothing for other entity types
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Script for removing followers from TestCases when removed from their parent tables.
|
||||||
|
// TestCases can only have inherited followers, so when the parent loses followers,
|
||||||
|
// we need to update the TestCase's follower list accordingly.
|
||||||
|
// Note: deletedFollowers contains the REMAINING followers after deletion, not the deleted ones.
|
||||||
|
// This script only applies to TestCases - does nothing for other entity types.
|
||||||
|
String REMOVE_FOLLOWERS_SCRIPT =
|
||||||
|
"""
|
||||||
|
if (ctx._source.containsKey('entityType') && ctx._source.entityType == 'testCase') {
|
||||||
|
// For TestCases, replace with the updated follower list (already has removed followers filtered out)
|
||||||
|
if (params.containsKey('deletedFollowers') && params.deletedFollowers != null) {
|
||||||
|
List followerIds = new ArrayList();
|
||||||
|
for (def follower : params.deletedFollowers) {
|
||||||
|
if (follower != null && follower.containsKey('id')) {
|
||||||
|
followerIds.add(follower.id.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx._source.followers = followerIds;
|
||||||
|
} else {
|
||||||
|
// If no followers remain, clear the list
|
||||||
|
ctx._source.followers = new ArrayList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do nothing for other entity types
|
||||||
|
""";
|
||||||
|
|
||||||
String UPDATE_TAGS_FIELD_SCRIPT =
|
String UPDATE_TAGS_FIELD_SCRIPT =
|
||||||
"""
|
"""
|
||||||
if (ctx._source.tags != null) {
|
if (ctx._source.tags != null) {
|
||||||
|
@ -21,6 +21,18 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
|
|
||||||
static final String SEARCH_LIST_FILTER_EXCLUDE = "fqnParts,entityType,suggest";
|
static final String SEARCH_LIST_FILTER_EXCLUDE = "fqnParts,entityType,suggest";
|
||||||
|
|
||||||
|
// Elasticsearch field name constants
|
||||||
|
private static final String FIELD_DELETED = "deleted";
|
||||||
|
private static final String FIELD_OWNERS_ID = "owners.id";
|
||||||
|
private static final String FIELD_CREATED_BY = "createdBy";
|
||||||
|
private static final String FIELD_DOMAINS_FQN = "domains.fullyQualifiedName";
|
||||||
|
private static final String FIELD_SERVICE_NAME = "service.name";
|
||||||
|
private static final String FIELD_TEST_CASE_STATUS = "testCaseResult.testCaseStatus";
|
||||||
|
private static final String FIELD_TEST_PLATFORMS = "testPlatforms";
|
||||||
|
private static final String FIELD_FOLLOWERS_KEYWORD = "followers.keyword";
|
||||||
|
private static final String FIELD_TEST_STATUS = "testCaseStatus";
|
||||||
|
private static final String FIELD_BASIC = "basic";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCondition(String entityType) {
|
public String getCondition(String entityType) {
|
||||||
String conditionFilter = buildConditionFilter(entityType);
|
String conditionFilter = buildConditionFilter(entityType);
|
||||||
@ -115,7 +127,7 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
String domain = getQueryParam("domains");
|
String domain = getQueryParam("domains");
|
||||||
if (!nullOrEmpty(domain)) {
|
if (!nullOrEmpty(domain)) {
|
||||||
return String.format(
|
return String.format(
|
||||||
"{\"term\": {\"domains.fullyQualifiedName\": \"%s\"}}", escapeDoubleQuotes(domain));
|
"{\"term\": {\"%s\": \"%s\"}}", FIELD_DOMAINS_FQN, escapeDoubleQuotes(domain));
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -129,7 +141,8 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
}
|
}
|
||||||
String deleted = "";
|
String deleted = "";
|
||||||
if (include != Include.ALL && supportsDeleted) {
|
if (include != Include.ALL && supportsDeleted) {
|
||||||
deleted = String.format("{\"term\": {\"deleted\": \"%s\"}}", include == Include.DELETED);
|
deleted =
|
||||||
|
String.format("{\"term\": {\"%s\": \"%s\"}}", FIELD_DELETED, include == Include.DELETED);
|
||||||
}
|
}
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
@ -139,7 +152,7 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
if (!nullOrEmpty(owners)) {
|
if (!nullOrEmpty(owners)) {
|
||||||
String ownersList =
|
String ownersList =
|
||||||
Arrays.stream(owners.split(",")).collect(Collectors.joining("\", \"", "\"", "\""));
|
Arrays.stream(owners.split(",")).collect(Collectors.joining("\", \"", "\"", "\""));
|
||||||
return String.format("{\"terms\": {\"owners.id\": [%s]}}", ownersList);
|
return String.format("{\"terms\": {\"%s\": [%s]}}", FIELD_OWNERS_ID, ownersList);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -147,7 +160,8 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
private String getCreatedByCondition() {
|
private String getCreatedByCondition() {
|
||||||
String createdBy = getQueryParam("createdBy");
|
String createdBy = getQueryParam("createdBy");
|
||||||
if (!nullOrEmpty(createdBy)) {
|
if (!nullOrEmpty(createdBy)) {
|
||||||
return String.format("{\"term\": {\"createdBy\": \"%s\"}}", escapeDoubleQuotes(createdBy));
|
return String.format(
|
||||||
|
"{\"term\": {\"%s\": \"%s\"}}", FIELD_CREATED_BY, escapeDoubleQuotes(createdBy));
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -200,6 +214,7 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
String tier = getQueryParam("tier");
|
String tier = getQueryParam("tier");
|
||||||
String serviceName = getQueryParam("serviceName");
|
String serviceName = getQueryParam("serviceName");
|
||||||
String dataQualityDimension = getQueryParam("dataQualityDimension");
|
String dataQualityDimension = getQueryParam("dataQualityDimension");
|
||||||
|
String followedBy = getQueryParam("followedBy");
|
||||||
|
|
||||||
if (tags != null) {
|
if (tags != null) {
|
||||||
String tagsList =
|
String tagsList =
|
||||||
@ -221,7 +236,8 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
|
|
||||||
if (serviceName != null) {
|
if (serviceName != null) {
|
||||||
conditions.add(
|
conditions.add(
|
||||||
String.format("{\"term\": {\"service.name\": \"%s\"}}", escapeDoubleQuotes(serviceName)));
|
String.format(
|
||||||
|
"{\"term\": {\"%s\": \"%s\"}}", FIELD_SERVICE_NAME, escapeDoubleQuotes(serviceName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityFQN != null) {
|
if (entityFQN != null) {
|
||||||
@ -235,8 +251,7 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
if (testSuiteId != null) conditions.add(getTestSuiteIdCondition(testSuiteId));
|
if (testSuiteId != null) conditions.add(getTestSuiteIdCondition(testSuiteId));
|
||||||
|
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
conditions.add(
|
conditions.add(String.format("{\"term\": {\"%s\": \"%s\"}}", FIELD_TEST_CASE_STATUS, status));
|
||||||
String.format("{\"term\": {\"testCaseResult.testCaseStatus\": \"%s\"}}", status));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type != null) conditions.add(getTestCaseTypeCondition(type, "entityLink"));
|
if (type != null) conditions.add(getTestCaseTypeCondition(type, "entityLink"));
|
||||||
@ -244,7 +259,7 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
if (testPlatform != null) {
|
if (testPlatform != null) {
|
||||||
String platforms =
|
String platforms =
|
||||||
Arrays.stream(testPlatform.split(",")).collect(Collectors.joining("\", \"", "\"", "\""));
|
Arrays.stream(testPlatform.split(",")).collect(Collectors.joining("\", \"", "\"", "\""));
|
||||||
conditions.add(String.format("{\"terms\": {\"testPlatforms\": [%s]}}", platforms));
|
conditions.add(String.format("{\"terms\": {\"%s\": [%s]}}", FIELD_TEST_PLATFORMS, platforms));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startTimestamp != null && endTimestamp != null) {
|
if (startTimestamp != null && endTimestamp != null) {
|
||||||
@ -258,6 +273,11 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
conditions.add(
|
conditions.add(
|
||||||
getDataQualityDimensionCondition(dataQualityDimension, "dataQualityDimension"));
|
getDataQualityDimensionCondition(dataQualityDimension, "dataQualityDimension"));
|
||||||
|
|
||||||
|
if (followedBy != null) {
|
||||||
|
conditions.add(
|
||||||
|
String.format("{\"term\": {\"%s\": \"%s\"}}", FIELD_FOLLOWERS_KEYWORD, followedBy));
|
||||||
|
}
|
||||||
|
|
||||||
return addCondition(conditions);
|
return addCondition(conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,7 +309,8 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
escapeDoubleQuotes(testCaseFQN)));
|
escapeDoubleQuotes(testCaseFQN)));
|
||||||
}
|
}
|
||||||
if (testCaseStatus != null)
|
if (testCaseStatus != null)
|
||||||
conditions.add(String.format("{\"term\": {\"testCaseStatus\": \"%s\"}}", testCaseStatus));
|
conditions.add(
|
||||||
|
String.format("{\"term\": {\"%s\": \"%s\"}}", FIELD_TEST_STATUS, testCaseStatus));
|
||||||
if (type != null) conditions.add(getTestCaseTypeCondition(type, "testCase.entityLink"));
|
if (type != null) conditions.add(getTestCaseTypeCondition(type, "testCase.entityLink"));
|
||||||
if (testSuiteId != null) conditions.add(getTestSuiteIdCondition(testSuiteId));
|
if (testSuiteId != null) conditions.add(getTestSuiteIdCondition(testSuiteId));
|
||||||
if (dataQualityDimension != null)
|
if (dataQualityDimension != null)
|
||||||
@ -308,7 +329,7 @@ public class SearchListFilter extends Filter<SearchListFilter> {
|
|||||||
|
|
||||||
if (testSuiteType != null) {
|
if (testSuiteType != null) {
|
||||||
boolean basic = !testSuiteType.equals("logical");
|
boolean basic = !testSuiteType.equals("logical");
|
||||||
conditions.add(String.format("{\"term\": {\"basic\": \"%s\"}}", basic));
|
conditions.add(String.format("{\"term\": {\"%s\": \"%s\"}}", FIELD_BASIC, basic));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!includeEmptyTestSuites) {
|
if (!includeEmptyTestSuites) {
|
||||||
|
@ -16,6 +16,7 @@ import static org.openmetadata.service.Entity.RAW_COST_ANALYSIS_REPORT_DATA;
|
|||||||
import static org.openmetadata.service.Entity.WEB_ANALYTIC_ENTITY_VIEW_REPORT_DATA;
|
import static org.openmetadata.service.Entity.WEB_ANALYTIC_ENTITY_VIEW_REPORT_DATA;
|
||||||
import static org.openmetadata.service.Entity.WEB_ANALYTIC_USER_ACTIVITY_REPORT_DATA;
|
import static org.openmetadata.service.Entity.WEB_ANALYTIC_USER_ACTIVITY_REPORT_DATA;
|
||||||
import static org.openmetadata.service.search.SearchClient.ADD_DOMAINS_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.ADD_DOMAINS_SCRIPT;
|
||||||
|
import static org.openmetadata.service.search.SearchClient.ADD_FOLLOWERS_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.ADD_OWNERS_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.ADD_OWNERS_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.DATA_ASSET_SEARCH_ALIAS;
|
import static org.openmetadata.service.search.SearchClient.DATA_ASSET_SEARCH_ALIAS;
|
||||||
import static org.openmetadata.service.search.SearchClient.DEFAULT_UPDATE_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.DEFAULT_UPDATE_SCRIPT;
|
||||||
@ -28,6 +29,7 @@ import static org.openmetadata.service.search.SearchClient.REMOVE_DATA_PRODUCTS_
|
|||||||
import static org.openmetadata.service.search.SearchClient.REMOVE_DOMAINS_CHILDREN_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.REMOVE_DOMAINS_CHILDREN_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.REMOVE_DOMAINS_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.REMOVE_DOMAINS_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.REMOVE_ENTITY_RELATIONSHIP;
|
import static org.openmetadata.service.search.SearchClient.REMOVE_ENTITY_RELATIONSHIP;
|
||||||
|
import static org.openmetadata.service.search.SearchClient.REMOVE_FOLLOWERS_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.REMOVE_OWNERS_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.REMOVE_OWNERS_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.REMOVE_PROPAGATED_ENTITY_REFERENCE_FIELD_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.REMOVE_PROPAGATED_ENTITY_REFERENCE_FIELD_SCRIPT;
|
||||||
import static org.openmetadata.service.search.SearchClient.REMOVE_PROPAGATED_FIELD_SCRIPT;
|
import static org.openmetadata.service.search.SearchClient.REMOVE_PROPAGATED_FIELD_SCRIPT;
|
||||||
@ -139,6 +141,7 @@ public class SearchRepository {
|
|||||||
List.of(
|
List.of(
|
||||||
FIELD_OWNERS,
|
FIELD_OWNERS,
|
||||||
FIELD_DOMAINS,
|
FIELD_DOMAINS,
|
||||||
|
FIELD_FOLLOWERS,
|
||||||
Entity.FIELD_DISABLED,
|
Entity.FIELD_DISABLED,
|
||||||
Entity.FIELD_TEST_SUITES,
|
Entity.FIELD_TEST_SUITES,
|
||||||
FIELD_DISPLAY_NAME);
|
FIELD_DISPLAY_NAME);
|
||||||
@ -781,6 +784,12 @@ public class SearchRepository {
|
|||||||
fieldData.put("deletedDomains", inheritedDomains);
|
fieldData.put("deletedDomains", inheritedDomains);
|
||||||
scriptTxt.append(REMOVE_DOMAINS_SCRIPT);
|
scriptTxt.append(REMOVE_DOMAINS_SCRIPT);
|
||||||
scriptTxt.append(" ");
|
scriptTxt.append(" ");
|
||||||
|
} else if (field.getName().equals(FIELD_FOLLOWERS)) {
|
||||||
|
List<EntityReference> inheritedFollowers =
|
||||||
|
copyWithInheritedFlag(entity.getFollowers());
|
||||||
|
fieldData.put("deletedFollowers", inheritedFollowers);
|
||||||
|
scriptTxt.append(REMOVE_FOLLOWERS_SCRIPT);
|
||||||
|
scriptTxt.append(" ");
|
||||||
} else {
|
} else {
|
||||||
EntityReference entityReference =
|
EntityReference entityReference =
|
||||||
JsonUtils.readValue(field.getOldValue().toString(), EntityReference.class);
|
JsonUtils.readValue(field.getOldValue().toString(), EntityReference.class);
|
||||||
@ -852,6 +861,11 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
fieldData.put("updatedDomains", inheritedDomains);
|
fieldData.put("updatedDomains", inheritedDomains);
|
||||||
scriptTxt.append(ADD_DOMAINS_SCRIPT);
|
scriptTxt.append(ADD_DOMAINS_SCRIPT);
|
||||||
|
} else if (field.getName().equals(FIELD_FOLLOWERS)) {
|
||||||
|
List<EntityReference> inheritedFollowers =
|
||||||
|
copyWithInheritedFlag(entity.getFollowers());
|
||||||
|
fieldData.put("updatedFollowers", inheritedFollowers);
|
||||||
|
scriptTxt.append(ADD_FOLLOWERS_SCRIPT);
|
||||||
} else {
|
} else {
|
||||||
EntityReference entityReference =
|
EntityReference entityReference =
|
||||||
JsonUtils.readValue(field.getNewValue().toString(), EntityReference.class);
|
JsonUtils.readValue(field.getNewValue().toString(), EntityReference.class);
|
||||||
@ -1497,4 +1511,14 @@ public class SearchRepository {
|
|||||||
return searchClient.getSchemaEntityRelationship(
|
return searchClient.getSchemaEntityRelationship(
|
||||||
schemaFqn, queryFilter, includeSourceFields, offset, limit, from, size, deleted);
|
schemaFqn, queryFilter, includeSourceFields, offset, limit, from, size, deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<EntityReference> copyWithInheritedFlag(List<EntityReference> references) {
|
||||||
|
if (references == null || references.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<EntityReference> inheritedReferences =
|
||||||
|
JsonUtils.deepCopyList(references, EntityReference.class);
|
||||||
|
inheritedReferences.forEach(ref -> ref.setInherited(true));
|
||||||
|
return inheritedReferences;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
|||||||
private final String allFields;
|
private final String allFields;
|
||||||
private final String
|
private final String
|
||||||
systemEntityName; // System entity provided by the system that can't be deleted
|
systemEntityName; // System entity provided by the system that can't be deleted
|
||||||
protected final boolean supportsFollowers;
|
protected boolean supportsFollowers;
|
||||||
protected final boolean supportsVotes;
|
protected final boolean supportsVotes;
|
||||||
protected boolean supportsOwners;
|
protected boolean supportsOwners;
|
||||||
protected boolean supportsTags;
|
protected boolean supportsTags;
|
||||||
@ -4160,6 +4160,45 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
|||||||
return responseMap.get();
|
return responseMap.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a specific field to have an expected value in the search index.
|
||||||
|
* This is useful for waiting for inherited fields to propagate.
|
||||||
|
*
|
||||||
|
* @param entityId The entity ID to check
|
||||||
|
* @param entityType The entity type
|
||||||
|
* @param fieldName The field name to check
|
||||||
|
* @param expectedValue The expected value of the field
|
||||||
|
*/
|
||||||
|
public static void waitForFieldInSearchIndex(
|
||||||
|
UUID entityId, String entityType, String fieldName, Object expectedValue) {
|
||||||
|
Awaitility.await(String.format("Wait for field '%s' to be updated in search index", fieldName))
|
||||||
|
.ignoreExceptions()
|
||||||
|
.pollInterval(Duration.ofMillis(500))
|
||||||
|
.atMost(Duration.ofSeconds(30))
|
||||||
|
.until(
|
||||||
|
() -> {
|
||||||
|
Map<String, Object> doc = getEntityDocumentFromSearch(entityId, entityType);
|
||||||
|
Object actualValue = doc.get(fieldName);
|
||||||
|
|
||||||
|
// Handle null comparisons
|
||||||
|
if (expectedValue == null) {
|
||||||
|
return actualValue == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For collections, compare contents
|
||||||
|
if (expectedValue instanceof List && actualValue instanceof List) {
|
||||||
|
List<?> expectedList = (List<?>) expectedValue;
|
||||||
|
List<?> actualList = (List<?>) actualValue;
|
||||||
|
return expectedList.size() == actualList.size()
|
||||||
|
&& expectedList.containsAll(actualList)
|
||||||
|
&& actualList.containsAll(expectedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other types, use equals
|
||||||
|
return expectedValue.equals(actualValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<String, Object> getEntityDocumentFromSearch(UUID entityId, String entityType)
|
public static Map<String, Object> getEntityDocumentFromSearch(UUID entityId, String entityType)
|
||||||
throws HttpResponseException {
|
throws HttpResponseException {
|
||||||
IndexMapping indexMapping = Entity.getSearchRepository().getIndexMapping(entityType);
|
IndexMapping indexMapping = Entity.getSearchRepository().getIndexMapping(entityType);
|
||||||
|
@ -195,6 +195,8 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
|
|||||||
"dataQuality/testCases",
|
"dataQuality/testCases",
|
||||||
TestCaseResource.FIELDS);
|
TestCaseResource.FIELDS);
|
||||||
supportsTags = false; // Test cases do not support setting tags directly (inherits from Entity)
|
supportsTags = false; // Test cases do not support setting tags directly (inherits from Entity)
|
||||||
|
supportsFollowers =
|
||||||
|
false; // Test cases do not support setting followers directly (inherits from parent table)
|
||||||
testCaseResultsCollectionName = "dataQuality/testCases/testCaseResults";
|
testCaseResultsCollectionName = "dataQuality/testCases/testCaseResults";
|
||||||
supportsEtag = false;
|
supportsEtag = false;
|
||||||
}
|
}
|
||||||
@ -3155,6 +3157,21 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
|
|||||||
return TestUtils.get(target, TestCaseResultResource.TestCaseResultList.class, authHeader);
|
return TestUtils.get(target, TestCaseResultResource.TestCaseResultList.class, authHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ResultList<TestCase> listTestCasesFromSearch(
|
||||||
|
Map<String, String> queryParams,
|
||||||
|
Integer limit,
|
||||||
|
Integer offset,
|
||||||
|
Map<String, String> authHeader)
|
||||||
|
throws HttpResponseException {
|
||||||
|
WebTarget target = getCollection().path("/search/list");
|
||||||
|
for (Map.Entry<String, String> entry : queryParams.entrySet()) {
|
||||||
|
target = target.queryParam(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
target = limit != null ? target.queryParam("limit", limit) : target;
|
||||||
|
target = offset != null ? target.queryParam("offset", offset) : target;
|
||||||
|
return TestUtils.get(target, TestCaseResource.TestCaseList.class, authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
protected void validateListTestCaseResultsFromSearchWithPagination(
|
protected void validateListTestCaseResultsFromSearchWithPagination(
|
||||||
Map<String, String> queryParams, Integer maxEntities, String path) throws IOException {
|
Map<String, String> queryParams, Integer maxEntities, String path) throws IOException {
|
||||||
// List all entities and use it for checking pagination
|
// List all entities and use it for checking pagination
|
||||||
@ -4139,6 +4156,165 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
|
|||||||
assertNull(nullValueResult.getFailedRows(), "Failed rows should be null when not set");
|
assertNull(nullValueResult.getFailedRows(), "Failed rows should be null when not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_testCaseFollowerInheritance(TestInfo testInfo)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
// Create a table with a follower
|
||||||
|
TableResourceTest tableResourceTest = new TableResourceTest();
|
||||||
|
CreateTable tableReq =
|
||||||
|
tableResourceTest
|
||||||
|
.createRequest(testInfo)
|
||||||
|
.withColumns(
|
||||||
|
List.of(new Column().withName(C1).withDisplayName("c1").withDataType(BIGINT)));
|
||||||
|
Table table = tableResourceTest.createAndCheckEntity(tableReq, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Add USER1 as follower to the table
|
||||||
|
tableResourceTest.addFollower(table.getId(), USER1_REF.getId(), OK, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Create a test case for this table
|
||||||
|
CreateTestCase create =
|
||||||
|
createRequest(testInfo)
|
||||||
|
.withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName()))
|
||||||
|
.withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName())
|
||||||
|
.withParameterValues(
|
||||||
|
List.of(new TestCaseParameterValue().withValue("10").withName("minValue")));
|
||||||
|
TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Wait for search index sync
|
||||||
|
Map<String, Object> sourceAsMap =
|
||||||
|
waitForSyncAndGetFromSearchIndex(
|
||||||
|
testCase.getUpdatedAt(), testCase.getId(), Entity.TEST_CASE);
|
||||||
|
|
||||||
|
// Verify the test case inherited the follower from the table
|
||||||
|
List<String> followers = (List<String>) sourceAsMap.get("followers");
|
||||||
|
assertNotNull(followers);
|
||||||
|
assertEquals(1, followers.size());
|
||||||
|
assertEquals(USER1.getId().toString(), followers.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_listTestCasesWithFollowedByFilter(TestInfo testInfo)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
// Create two tables with different followers
|
||||||
|
TableResourceTest tableResourceTest = new TableResourceTest();
|
||||||
|
CreateTable tableReq1 =
|
||||||
|
tableResourceTest
|
||||||
|
.createRequest(testInfo, 1)
|
||||||
|
.withColumns(
|
||||||
|
List.of(new Column().withName(C1).withDisplayName("c1").withDataType(BIGINT)));
|
||||||
|
Table table1 = tableResourceTest.createAndCheckEntity(tableReq1, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
CreateTable tableReq2 =
|
||||||
|
tableResourceTest
|
||||||
|
.createRequest(testInfo, 2)
|
||||||
|
.withColumns(
|
||||||
|
List.of(new Column().withName(C1).withDisplayName("c1").withDataType(BIGINT)));
|
||||||
|
Table table2 = tableResourceTest.createAndCheckEntity(tableReq2, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Add USER1 as follower to table1
|
||||||
|
tableResourceTest.addFollower(table1.getId(), USER1_REF.getId(), OK, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Add USER2 as follower to table2
|
||||||
|
tableResourceTest.addFollower(table2.getId(), USER2_REF.getId(), OK, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Create test cases for both tables
|
||||||
|
CreateTestCase create1 =
|
||||||
|
createRequest(testInfo, 1)
|
||||||
|
.withEntityLink(String.format("<#E::table::%s>", table1.getFullyQualifiedName()))
|
||||||
|
.withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName())
|
||||||
|
.withParameterValues(
|
||||||
|
List.of(new TestCaseParameterValue().withValue("10").withName("minValue")));
|
||||||
|
TestCase testCase1 = createAndCheckEntity(create1, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
CreateTestCase create2 =
|
||||||
|
createRequest(testInfo, 2)
|
||||||
|
.withEntityLink(String.format("<#E::table::%s>", table2.getFullyQualifiedName()))
|
||||||
|
.withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName())
|
||||||
|
.withParameterValues(
|
||||||
|
List.of(new TestCaseParameterValue().withValue("10").withName("minValue")));
|
||||||
|
TestCase testCase2 = createAndCheckEntity(create2, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Wait for test cases to be properly indexed with their inherited followers
|
||||||
|
// TestCase1 should inherit USER1 from table1
|
||||||
|
waitForFieldInSearchIndex(
|
||||||
|
testCase1.getId(), Entity.TEST_CASE, "followers", List.of(USER1.getId().toString()));
|
||||||
|
|
||||||
|
// TestCase2 should inherit USER2 from table2
|
||||||
|
waitForFieldInSearchIndex(
|
||||||
|
testCase2.getId(), Entity.TEST_CASE, "followers", List.of(USER2.getId().toString()));
|
||||||
|
|
||||||
|
// Test filtering by USER1
|
||||||
|
Map<String, String> queryParams = new HashMap<>();
|
||||||
|
queryParams.put("followedBy", USER1.getName());
|
||||||
|
ResultList<TestCase> results = listTestCasesFromSearch(queryParams, 10, 0, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Should find test cases from table1 (followed by USER1)
|
||||||
|
assertTrue(
|
||||||
|
results.getData().stream()
|
||||||
|
.anyMatch(tc -> tc.getFullyQualifiedName().equals(testCase1.getFullyQualifiedName())),
|
||||||
|
"TestCase1 should be found when filtering by USER1");
|
||||||
|
assertFalse(
|
||||||
|
results.getData().stream()
|
||||||
|
.anyMatch(tc -> tc.getFullyQualifiedName().equals(testCase2.getFullyQualifiedName())),
|
||||||
|
"TestCase2 should not be found when filtering by USER1");
|
||||||
|
|
||||||
|
// Test filtering by USER2
|
||||||
|
queryParams.put("followedBy", USER2.getName());
|
||||||
|
results = listTestCasesFromSearch(queryParams, 10, 0, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Should find test cases from table2 (followed by USER2)
|
||||||
|
assertFalse(
|
||||||
|
results.getData().stream()
|
||||||
|
.anyMatch(tc -> tc.getFullyQualifiedName().equals(testCase1.getFullyQualifiedName())),
|
||||||
|
"TestCase1 should not be found when filtering by USER2");
|
||||||
|
assertTrue(
|
||||||
|
results.getData().stream()
|
||||||
|
.anyMatch(tc -> tc.getFullyQualifiedName().equals(testCase2.getFullyQualifiedName())),
|
||||||
|
"TestCase2 should be found when filtering by USER2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_followerInheritanceAfterTableUpdate(TestInfo testInfo)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
// Create a table without followers
|
||||||
|
TableResourceTest tableResourceTest = new TableResourceTest();
|
||||||
|
CreateTable tableReq =
|
||||||
|
tableResourceTest
|
||||||
|
.createRequest(testInfo)
|
||||||
|
.withColumns(
|
||||||
|
List.of(new Column().withName(C1).withDisplayName("c1").withDataType(BIGINT)));
|
||||||
|
Table table = tableResourceTest.createAndCheckEntity(tableReq, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Create a test case for this table
|
||||||
|
CreateTestCase create =
|
||||||
|
createRequest(testInfo)
|
||||||
|
.withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName()))
|
||||||
|
.withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName())
|
||||||
|
.withParameterValues(
|
||||||
|
List.of(new TestCaseParameterValue().withValue("10").withName("minValue")));
|
||||||
|
TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Wait for initial sync
|
||||||
|
waitForSyncAndGetFromSearchIndex(testCase.getUpdatedAt(), testCase.getId(), Entity.TEST_CASE);
|
||||||
|
|
||||||
|
// Now add USER1 as follower to the table
|
||||||
|
tableResourceTest.addFollower(table.getId(), USER1_REF.getId(), OK, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// Wait for the follower to propagate to the test case in the search index
|
||||||
|
List<String> expectedFollowers = List.of(USER1.getId().toString());
|
||||||
|
waitForFieldInSearchIndex(testCase.getId(), Entity.TEST_CASE, "followers", expectedFollowers);
|
||||||
|
|
||||||
|
// Verify test cases now show up when filtering by USER1
|
||||||
|
Map<String, String> queryParams = new HashMap<>();
|
||||||
|
queryParams.put("followedBy", USER1.getName());
|
||||||
|
ResultList<TestCase> results = listTestCasesFromSearch(queryParams, 10, 0, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
results.getData().stream()
|
||||||
|
.anyMatch(tc -> tc.getFullyQualifiedName().equals(testCase.getFullyQualifiedName())),
|
||||||
|
"Test case should be found after table follower update");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test_entityStatusUpdateAndPatch(TestInfo test) throws IOException {
|
void test_entityStatusUpdateAndPatch(TestInfo test) throws IOException {
|
||||||
// Create a test case with APPROVED status by default
|
// Create a test case with APPROVED status by default
|
||||||
|
@ -139,6 +139,10 @@
|
|||||||
"description": "Domains the test case belongs to. When not set, the test case inherits the domain from the table it belongs to.",
|
"description": "Domains the test case belongs to. When not set, the test case inherits the domain from the table it belongs to.",
|
||||||
"$ref": "../type/entityReferenceList.json"
|
"$ref": "../type/entityReferenceList.json"
|
||||||
},
|
},
|
||||||
|
"followers": {
|
||||||
|
"description": "Followers of this test case. When not set, the test case inherits the followers from the table it belongs to.",
|
||||||
|
"$ref": "../type/entityReferenceList.json"
|
||||||
|
},
|
||||||
"useDynamicAssertion": {
|
"useDynamicAssertion": {
|
||||||
"description": "If the test definition supports it, use dynamic assertion to evaluate the test case.",
|
"description": "If the test definition supports it, use dynamic assertion to evaluate the test case.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -57,6 +57,11 @@ export interface TestCase {
|
|||||||
* Sample of failed rows for this test case.
|
* Sample of failed rows for this test case.
|
||||||
*/
|
*/
|
||||||
failedRowsSample?: TableData;
|
failedRowsSample?: TableData;
|
||||||
|
/**
|
||||||
|
* Followers of this test case. When not set, the test case inherits the followers from the
|
||||||
|
* table it belongs to.
|
||||||
|
*/
|
||||||
|
followers?: EntityReference[];
|
||||||
/**
|
/**
|
||||||
* FullyQualifiedName same as `name`.
|
* FullyQualifiedName same as `name`.
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user