Skip to content

Commit 77f9726

Browse files
authored
Fix #27107: soft-deleted users still appear in Experts/Reviewers across all entities (#27120)
* Fix #27107: soft-deleted users still appear in experts/reviewers across all entities Two bugs caused soft-deleted users to leak into experts/reviewers/owners/followers: 1. EntityRepository.resolveRelationshipEntityReferencesByType hardcoded Include.ALL for the consolidated bulk list path, affecting ALL entities (DataProduct, Domain, GlossaryTerm, Glossary, Table, Schema, Service, Container, etc.). Fixed by switching to getEntityReferencesByIdsRespectingInclude(..., NON_DELETED). 2. EntityResource.getInternal / getByNameInternal built RelationIncludes(null, ...) which defaulted to Include.ALL, causing single-entity GET on DataProduct, Domain, EventSubscription, Query to return deleted users in relation fields. Fixed by defaulting null include to Include.NON_DELETED for all resource endpoints. Also cleaned up dead-code in DataProductRepository.batchFetchExperts and DomainRepository.batchFetchExperts which used getEntityReferenceById that silently overrides NON_DELETED to ALL. Adds regression tests in DataProductResourceIT, DomainResourceIT, GlossaryTermResourceIT covering both single-GET and list-endpoint paths. * Thread Include through bulk relationship resolution instead of hardcoding NON_DELETED - Add Include parameter to resolveRelationshipEntityReferencesByType, fetchAndSetRelationshipFieldsInBulk, and fetchAndSetFields (new 3-arg overload) - Add setFieldsInBulk(fields, entities, Include) overload in base class; existing 2-arg delegates to NON_DELETED - The Include is no longer hardcoded — callers with request context (e.g. include=all) can pass it through via the 3-arg setFieldsInBulk or fetchAndSetFields Fixes: #27107 * Fix inherited experts/owners including soft-deleted users from parent Domain DataProductRepository.setInheritedFields and DomainRepository.setInheritedFields both fetched parent Domain entities with Include.ALL, causing the domain's experts and owners fields to be resolved with ALL include — returning soft-deleted users. Changed to NON_DELETED so inherited fields only carry active users. Fixes: #27107 * Fix soft-deleted users leaking into owners/experts/reviewers across all entities Five additional bug sites using Include.ALL when resolving user relationships: - EntityRepository.batchFetchOwners: getEntityReferencesByIds → getEntityReferencesByIdsRespectingInclude(NON_DELETED) - EntityRepository.batchFetchReviewers: same fix - EntityRepository.batchFetchExperts (base class): same fix - TagRepository.setInheritedFields: fetch Classification with NON_DELETED (was ALL), preventing soft-deleted owners/reviewers from being inherited by Tags - WorksheetRepository.setInheritedFields: fetch Spreadsheet with NON_DELETED (was ALL), preventing soft-deleted owners from being inherited by Worksheets Fixes: #27107 * Fix test user email validation in soft-delete integration tests ns.prefix() generates ~110-char strings; after stripping underscores the local part exceeds RFC 5321's 64-char limit and the server's maxLength:127 constraint. Switch to ns.shortPrefix() (~27 chars total) which is already alphanumeric+underscore and requires no sanitisation. * Fix GlossaryTerm list test to filter by glossary ID Without a glossary filter the list call returns the first 100 terms across all glossaries; in a populated CI env the test term lands outside that window and the search fails with 'GlossaryTerm not found in list'. Add .addFilter("glossary", glossary.getId()) so the query is scoped to only our test glossary. * Fix fromId/toId swap in EntityRepository.batchFetchExperts findToBatch(entityIds, EXPERT, USER) returns rows where fromId=entity and toId=user (matching the write path in bulkInsertToRelationship). The original code collected fromId as expert user IDs and toId as entity IDs — both backwards. Method is preempted by the bulk path in practice, but the logic should be correct. * Add merge function to Collectors.toMap in batchFetchExperts Prevents potential IllegalStateException if getEntityReferencesByIdsRespectingInclude returns duplicate references for the same ID due to data inconsistency. Consistent with the (a, b) -> a merge function used elsewhere. * Handle soft-deleted parent in setInheritedFields for DataProduct and Domain Entity.getEntity(..., NON_DELETED) throws EntityNotFoundException when the parent domain/domain is soft-deleted while the relationship still exists. Catch and skip inheritance for that parent rather than propagating the exception and breaking the GET/list response. * Remove dead setFieldsInBulk(Include) overload and collapse Include from private chain The 3-arg setFieldsInBulk(Fields, List<T>, Include) overload had no callers passing a non-default Include — the intent of this PR is to always filter soft-deleted users from relationship fields, not to respect the caller's include. Remove the overload and collapse the Include parameter out of fetchAndSetFields and fetchAndSetRelationshipFieldsInBulk/ resolveRelationshipEntityReferencesByType, hardcoding NON_DELETED where it matters. * Paginate domain list to find test domain regardless of total count * Revert WorksheetRepository Include change — out of scope for #27107 * Address PR review: add limit to DataProduct list test; align TagRepository bulk path to NON_DELETED * Revert TagRepository Include changes — out of scope for #27107 * Remove unnecessary changes and compilation error * Thread Include parameter through bulk relationship resolution path setFieldsInBulk, fetchAndSetFields, fetchAndSetRelationshipFieldsInBulk, and resolveRelationshipEntityReferencesByType now accept an Include parameter so that include=all list queries correctly surface soft-deleted relationship targets (owners, experts, reviewers) — matching the single-entity GET path behaviour via RelationIncludes. All existing 2-param overrides delegate to the new 3-param overloads with NON_DELETED, so all subclass repositories and the update hydration path are unchanged. All list/get callers with access to an Include value (listAfter, listBefore, listAll, listAfterKeyset, listWithOffset, get, getByNames) now forward that value through the chain. * Fix subclass bypass: use ThreadLocal to carry Include through virtual setFieldsInBulk dispatch The previous approach made callers invoke the 3-param setFieldsInBulk directly, which bypassed all subclass overrides of the 2-param version (GlossaryTermRepository, DatabaseRepository, TableRepository, etc. all override 2-param with essential hydration logic like populateParentAndGlossaryReferencesInBulk). This caused a regression where entities from those repos would be missing service references, parent references, and other custom bulk-hydrated fields. Fix: store the include in a static ThreadLocal before delegating to the virtual 2-param setFieldsInBulk (so all subclass overrides run), then the 2-param fetchAndSetFields reads bulkInclude.get() so it reaches fetchAndSetRelationshipFieldsInBulk with the correct value. The 3-param overload is now final to prevent further accidental overrides. * Fix null Include in setFieldsInBulk — default to NON_DELETED Callers like DomainResource/DataProductResource/PersonaResource build new ListFilter(null), so filter.getInclude() returns null. That null was stored in the bulkInclude ThreadLocal and propagated into getEntityReferencesByIdsRespectingInclude, bypassing the soft-delete filter for USER/TEAM types and re-introducing soft-deleted users in owners/experts/reviewers/followers on those list endpoints. Resolve null to NON_DELETED before setting the ThreadLocal. Add unit test covering null → NON_DELETED defaulting. * Fix #27107: fix inverted ternary root cause; harden bulk-list NON_DELETED Root cause: Entity.java had an inverted ternary in getEntityReferenceById and getEntityReferencesByIds: // WRONG — forces ALL when type supports soft delete include = repository.supportsSoftDelete ? Include.ALL : include; // CORRECT — respects caller's include; falls back to ALL only when the // type has no deleted column and filtering isn't possible include = repository.supportsSoftDelete ? include : Include.ALL; PR #25284 worked around this by introducing getEntityReferenceByIdRespectingInclude / getEntityReferencesByIdsRespectingInclude with the correct ternary, but the original broken methods were never fixed — a permanent trap. This commit fixes the root and deletes the RespectingInclude pair (now identical after the flip). For the bulk-list path (GET /entities?fields=...) nested reference hydration is hardcoded to NON_DELETED unconditionally in resolveRelationshipEntityReferencesByType and all five batchFetch* methods (owners, followers, reviewers, experts, votes). The ?include= query param controls which top-level entities appear in the list; it does not change the semantics of nested relationship pointers, which should always resolve to live entities. For single-entity GET: EntityResource.getInternal/getByNameInternal had a latent null-include bug for resources (Domain, DataProduct, EventSubscription, Query) that don't declare @QueryParam("include"). Those null values defaulted to ALL inside RelationIncludes, leaking soft-deleted nested refs. Fixed by defaulting null → NON_DELETED before constructing RelationIncludes. Remove ThreadLocal bulkInclude and the include-threading approach added earlier in this branch — superseded by the root-cause fix and the NON_DELETED semantic above. Delete EntityRepositoryIncludeThreadingTest (tested the rejected approach). Tests: 7 integration tests covering single-GET and list endpoints for Domain, DataProduct, and GlossaryTerm; verify soft-deleted experts/owners/reviewers are absent from both paths, including with ?include=all on the list endpoint. * Fix Copilot review: null-guard in batchFetchFollowers, remove dead 3-arg setFieldsInBulk, add follower/voter IT tests - batchFetchFollowers: add null-check before adding followerRef to list. When a follower is soft-deleted, getEntityReferencesByIds(NON_DELETED) omits their ID from the result map, so followerRefs.get(followerId) returns null. All other batchFetch* methods already guard this; batchFetchFollowers was the only one missing the check. - Remove the dead 3-arg setFieldsInBulk(Fields, List<T>, Include) overload that accepted include but silently discarded it, delegating to the 2-arg form. Revert all 6 call sites back to the 2-arg form. The overload was misleading — callers appeared to plumb include through but bulk hydration was never affected; NON_DELETED is hardcoded in resolveRelationship- EntityReferencesByType. - Add integration tests for followers and votes: softDeletedFollower_notReturnedInListEndpoint and softDeletedVoter_notReturnedInListEndpoint in DomainResourceIT. Both pass (5/5 DomainResourceIT soft-delete tests green). * fix: bump list limit to 1000000 in follower/voter IT tests to handle large CI datasets * fix: remove unused Include param from listInternal and serializeJsons
1 parent df9e5e0 commit 77f9726

9 files changed

Lines changed: 416 additions & 74 deletions

File tree

openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.openmetadata.schema.api.domains.CreateDomain.DomainType;
3434
import org.openmetadata.schema.api.domains.DataProductPortsView;
3535
import org.openmetadata.schema.api.services.CreateDatabaseService;
36+
import org.openmetadata.schema.api.teams.CreateUser;
3637
import org.openmetadata.schema.entity.data.Dashboard;
3738
import org.openmetadata.schema.entity.data.Table;
3839
import org.openmetadata.schema.entity.data.Topic;
@@ -41,6 +42,7 @@
4142
import org.openmetadata.schema.entity.services.DashboardService;
4243
import org.openmetadata.schema.entity.services.DatabaseService;
4344
import org.openmetadata.schema.entity.services.MessagingService;
45+
import org.openmetadata.schema.entity.teams.User;
4446
import org.openmetadata.schema.entity.type.Style;
4547
import org.openmetadata.schema.services.connections.database.MysqlConnection;
4648
import org.openmetadata.schema.services.connections.database.common.basicAuth;
@@ -2884,4 +2886,122 @@ void test_deletingAssetRemovesItFromPorts(TestNamespace ns) throws Exception {
28842886
ResultList<Map<String, Object>> outputPorts = getOutputPorts(dataProduct.getId(), 10, 0);
28852887
assertEquals(0, outputPorts.getPaging().getTotal());
28862888
}
2889+
2890+
@Test
2891+
void softDeletedExpert_notReturnedInSingleGet(TestNamespace ns) {
2892+
OpenMetadataClient client = SdkClients.adminClient();
2893+
Domain domain = getOrCreateDomain(ns);
2894+
2895+
String userName = ns.shortPrefix("expert_user");
2896+
User expert =
2897+
client
2898+
.users()
2899+
.create(
2900+
new CreateUser()
2901+
.withName(userName)
2902+
.withEmail(userName + "@test.openmetadata.org")
2903+
.withDescription("Expert user for soft-delete test"));
2904+
2905+
CreateDataProduct create =
2906+
new CreateDataProduct()
2907+
.withName(ns.prefix("dp_softdel_expert"))
2908+
.withDescription("DataProduct for soft-delete expert test")
2909+
.withDomains(List.of(domain.getFullyQualifiedName()))
2910+
.withExperts(List.of(expert.getFullyQualifiedName()));
2911+
DataProduct dp = createEntity(create);
2912+
2913+
client.users().delete(expert.getId().toString());
2914+
2915+
DataProduct byId = client.dataProducts().get(dp.getId().toString(), "experts");
2916+
assertTrue(
2917+
byId.getExperts() == null || byId.getExperts().isEmpty(),
2918+
"Soft-deleted expert must not appear in single GET by ID");
2919+
2920+
DataProduct byName = client.dataProducts().getByName(dp.getFullyQualifiedName(), "experts");
2921+
assertTrue(
2922+
byName.getExperts() == null || byName.getExperts().isEmpty(),
2923+
"Soft-deleted expert must not appear in single GET by name");
2924+
}
2925+
2926+
@Test
2927+
void softDeletedExpert_notReturnedInListEndpoint(TestNamespace ns) {
2928+
OpenMetadataClient client = SdkClients.adminClient();
2929+
Domain domain = getOrCreateDomain(ns);
2930+
2931+
String userName = ns.shortPrefix("expert_list_user");
2932+
User expert =
2933+
client
2934+
.users()
2935+
.create(
2936+
new CreateUser()
2937+
.withName(userName)
2938+
.withEmail(userName + "@test.openmetadata.org")
2939+
.withDescription("Expert user for bulk soft-delete test"));
2940+
2941+
CreateDataProduct create =
2942+
new CreateDataProduct()
2943+
.withName(ns.prefix("dp_softdel_expert_list"))
2944+
.withDescription("DataProduct for soft-delete expert list test")
2945+
.withDomains(List.of(domain.getFullyQualifiedName()))
2946+
.withExperts(List.of(expert.getFullyQualifiedName()));
2947+
DataProduct dp = createEntity(create);
2948+
2949+
client.users().delete(expert.getId().toString());
2950+
2951+
ListParams params =
2952+
new ListParams()
2953+
.setFields("experts")
2954+
.withDomain(domain.getFullyQualifiedName())
2955+
.withLimit(100);
2956+
ListResponse<DataProduct> list = client.dataProducts().list(params);
2957+
DataProduct listed =
2958+
list.getData().stream()
2959+
.filter(p -> p.getId().equals(dp.getId()))
2960+
.findFirst()
2961+
.orElseThrow(() -> new AssertionError("DataProduct not found in list"));
2962+
assertTrue(
2963+
listed.getExperts() == null || listed.getExperts().isEmpty(),
2964+
"Soft-deleted expert must not appear in list endpoint");
2965+
}
2966+
2967+
@Test
2968+
void softDeletedOwner_notReturnedInListEndpoint(TestNamespace ns) {
2969+
OpenMetadataClient client = SdkClients.adminClient();
2970+
Domain domain = getOrCreateDomain(ns);
2971+
2972+
String userName = ns.shortPrefix("owner_list_user");
2973+
User owner =
2974+
client
2975+
.users()
2976+
.create(
2977+
new CreateUser()
2978+
.withName(userName)
2979+
.withEmail(userName + "@test.openmetadata.org")
2980+
.withDescription("Owner user for soft-delete list test"));
2981+
2982+
CreateDataProduct create =
2983+
new CreateDataProduct()
2984+
.withName(ns.prefix("dp_softdel_owner_list"))
2985+
.withDescription("DataProduct for soft-delete owner list test")
2986+
.withDomains(List.of(domain.getFullyQualifiedName()))
2987+
.withOwners(List.of(owner.getEntityReference()));
2988+
DataProduct dp = createEntity(create);
2989+
2990+
client.users().delete(owner.getId().toString());
2991+
2992+
ListParams params =
2993+
new ListParams()
2994+
.setFields("owners")
2995+
.withDomain(domain.getFullyQualifiedName())
2996+
.withLimit(100);
2997+
ListResponse<DataProduct> list = client.dataProducts().list(params);
2998+
DataProduct listed =
2999+
list.getData().stream()
3000+
.filter(p -> p.getId().equals(dp.getId()))
3001+
.findFirst()
3002+
.orElseThrow(() -> new AssertionError("DataProduct not found in list"));
3003+
assertTrue(
3004+
listed.getOwners() == null || listed.getOwners().isEmpty(),
3005+
"Soft-deleted owner must not appear in list endpoint");
3006+
}
28873007
}

openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DomainResourceIT.java

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@
2929
import org.junit.jupiter.api.parallel.ExecutionMode;
3030
import org.openmetadata.it.util.SdkClients;
3131
import org.openmetadata.it.util.TestNamespace;
32+
import org.openmetadata.schema.api.VoteRequest;
3233
import org.openmetadata.schema.api.domains.CreateDomain;
3334
import org.openmetadata.schema.api.domains.CreateDomain.DomainType;
35+
import org.openmetadata.schema.api.teams.CreateUser;
3436
import org.openmetadata.schema.entity.domains.Domain;
37+
import org.openmetadata.schema.entity.teams.User;
38+
import org.openmetadata.schema.type.ChangeEvent;
3539
import org.openmetadata.schema.type.EntityHistory;
3640
import org.openmetadata.schema.type.EntityReference;
41+
import org.openmetadata.schema.type.Votes;
3742
import org.openmetadata.sdk.client.OpenMetadataClient;
3843
import org.openmetadata.sdk.models.ListParams;
3944
import org.openmetadata.sdk.models.ListResponse;
45+
import org.openmetadata.sdk.network.HttpMethod;
4046

4147
/**
4248
* Integration tests for Domain entity operations.
@@ -1164,4 +1170,204 @@ void test_renameDomainDoesNotAffectSimilarPrefixDomains(TestNamespace ns) throws
11641170
// Verify old child FQN no longer works
11651171
assertThrows(Exception.class, () -> getEntityByName(oldChildFqn));
11661172
}
1173+
1174+
@Test
1175+
void softDeletedExpert_notReturnedInSingleGet(TestNamespace ns) {
1176+
OpenMetadataClient client = SdkClients.adminClient();
1177+
1178+
String userName = ns.shortPrefix("domain_expert");
1179+
User expert =
1180+
client
1181+
.users()
1182+
.create(
1183+
new CreateUser()
1184+
.withName(userName)
1185+
.withEmail(userName + "@test.openmetadata.org")
1186+
.withDescription("Expert user for domain soft-delete test"));
1187+
1188+
CreateDomain create =
1189+
new CreateDomain()
1190+
.withName(ns.prefix("domain_softdel"))
1191+
.withDomainType(DomainType.AGGREGATE)
1192+
.withExperts(List.of(expert.getFullyQualifiedName()))
1193+
.withDescription("Domain for soft-delete expert test");
1194+
Domain domain = createEntity(create);
1195+
1196+
client.users().delete(expert.getId().toString());
1197+
1198+
Domain byId = client.domains().get(domain.getId().toString(), "experts");
1199+
assertTrue(
1200+
byId.getExperts() == null || byId.getExperts().isEmpty(),
1201+
"Soft-deleted expert must not appear in single GET by ID");
1202+
1203+
Domain byName = client.domains().getByName(domain.getFullyQualifiedName(), "experts");
1204+
assertTrue(
1205+
byName.getExperts() == null || byName.getExperts().isEmpty(),
1206+
"Soft-deleted expert must not appear in single GET by name");
1207+
}
1208+
1209+
@Test
1210+
void softDeletedExpert_notReturnedInListEndpoint(TestNamespace ns) {
1211+
OpenMetadataClient client = SdkClients.adminClient();
1212+
1213+
String userName = ns.shortPrefix("domain_expert_list");
1214+
User expert =
1215+
client
1216+
.users()
1217+
.create(
1218+
new CreateUser()
1219+
.withName(userName)
1220+
.withEmail(userName + "@test.openmetadata.org")
1221+
.withDescription("Expert user for domain list soft-delete test"));
1222+
1223+
CreateDomain create =
1224+
new CreateDomain()
1225+
.withName(ns.prefix("domain_softdel_list"))
1226+
.withDomainType(DomainType.AGGREGATE)
1227+
.withExperts(List.of(expert.getFullyQualifiedName()))
1228+
.withDescription("Domain for soft-delete expert list test");
1229+
Domain domain = createEntity(create);
1230+
1231+
client.users().delete(expert.getId().toString());
1232+
1233+
Domain listed = null;
1234+
ListParams params = new ListParams().setFields("experts").withLimit(100);
1235+
while (listed == null) {
1236+
ListResponse<Domain> page = listEntities(params);
1237+
listed =
1238+
page.getData().stream()
1239+
.filter(d -> d.getId().equals(domain.getId()))
1240+
.findFirst()
1241+
.orElse(null);
1242+
String after = page.getPaging() != null ? page.getPaging().getAfter() : null;
1243+
if (listed != null || after == null) break;
1244+
params = new ListParams().setFields("experts").withLimit(100).setAfter(after);
1245+
}
1246+
assertNotNull(listed, "Domain not found in list");
1247+
assertTrue(
1248+
listed.getExperts() == null || listed.getExperts().isEmpty(),
1249+
"Soft-deleted expert must not appear in list endpoint");
1250+
}
1251+
1252+
@Test
1253+
void softDeletedExpert_notReturnedInListWithIncludeAll(TestNamespace ns) {
1254+
OpenMetadataClient client = SdkClients.adminClient();
1255+
1256+
String userName = ns.shortPrefix("domain_expert_all");
1257+
User expert =
1258+
client
1259+
.users()
1260+
.create(
1261+
new CreateUser()
1262+
.withName(userName)
1263+
.withEmail(userName + "@test.openmetadata.org")
1264+
.withDescription("Expert user for domain include-all soft-delete test"));
1265+
1266+
CreateDomain create =
1267+
new CreateDomain()
1268+
.withName(ns.prefix("domain_softdel_all"))
1269+
.withDomainType(DomainType.AGGREGATE)
1270+
.withExperts(List.of(expert.getFullyQualifiedName()))
1271+
.withDescription("Domain for include-all soft-delete expert test");
1272+
Domain domain = createEntity(create);
1273+
1274+
client.users().delete(expert.getId().toString());
1275+
1276+
Domain listed = null;
1277+
ListParams params =
1278+
new ListParams().setFields("experts").withLimit(100).addFilter("include", "all");
1279+
while (listed == null) {
1280+
ListResponse<Domain> page = listEntities(params);
1281+
listed =
1282+
page.getData().stream()
1283+
.filter(d -> d.getId().equals(domain.getId()))
1284+
.findFirst()
1285+
.orElse(null);
1286+
String after = page.getPaging() != null ? page.getPaging().getAfter() : null;
1287+
if (listed != null || after == null) break;
1288+
params =
1289+
new ListParams()
1290+
.setFields("experts")
1291+
.withLimit(100)
1292+
.addFilter("include", "all")
1293+
.setAfter(after);
1294+
}
1295+
assertNotNull(listed, "Domain not found in list with include=all");
1296+
assertTrue(
1297+
listed.getExperts() == null || listed.getExperts().isEmpty(),
1298+
"Soft-deleted expert must not appear even when include=all (applies to top-level only)");
1299+
}
1300+
1301+
@Test
1302+
void softDeletedFollower_notReturnedInListEndpoint(TestNamespace ns) {
1303+
OpenMetadataClient client = SdkClients.adminClient();
1304+
1305+
String userName = ns.shortPrefix("follower_list");
1306+
User follower =
1307+
client
1308+
.users()
1309+
.create(
1310+
new CreateUser().withName(userName).withEmail(userName + "@test.openmetadata.org"));
1311+
1312+
Domain domain = createEntity(createRequest(ns.prefix("dom_follower"), ns));
1313+
1314+
client
1315+
.getHttpClient()
1316+
.execute(
1317+
HttpMethod.PUT,
1318+
"/v1/domains/" + domain.getId() + "/followers",
1319+
follower.getId(),
1320+
ChangeEvent.class);
1321+
1322+
client.users().delete(follower.getId().toString());
1323+
1324+
ListParams params = new ListParams().setFields("followers").withLimit(1000000);
1325+
ListResponse<Domain> list = listEntities(params);
1326+
Domain listed =
1327+
list.getData().stream()
1328+
.filter(d -> d.getId().equals(domain.getId()))
1329+
.findFirst()
1330+
.orElseThrow(() -> new AssertionError("Domain not found in list"));
1331+
assertTrue(
1332+
listed.getFollowers() == null || listed.getFollowers().isEmpty(),
1333+
"Soft-deleted follower must not appear in list endpoint");
1334+
}
1335+
1336+
@Test
1337+
void softDeletedVoter_notReturnedInListEndpoint(TestNamespace ns) {
1338+
String userName = ns.shortPrefix("voter_list");
1339+
String userEmail = userName + "@test.openmetadata.org";
1340+
1341+
OpenMetadataClient adminClient = SdkClients.adminClient();
1342+
User voter =
1343+
adminClient.users().create(new CreateUser().withName(userName).withEmail(userEmail));
1344+
1345+
Domain domain = createEntity(createRequest(ns.prefix("dom_voter"), ns));
1346+
1347+
OpenMetadataClient voterClient = SdkClients.createClient(userEmail, userEmail, new String[] {});
1348+
voterClient
1349+
.getHttpClient()
1350+
.execute(
1351+
HttpMethod.PUT,
1352+
"/v1/domains/" + domain.getId() + "/vote",
1353+
new VoteRequest().withUpdatedVoteType(VoteRequest.VoteType.VOTED_UP),
1354+
ChangeEvent.class);
1355+
1356+
adminClient.users().delete(voter.getId().toString());
1357+
1358+
ListParams params = new ListParams().setFields("votes").withLimit(1000000);
1359+
ListResponse<Domain> list = listEntities(params);
1360+
Domain listed =
1361+
list.getData().stream()
1362+
.filter(d -> d.getId().equals(domain.getId()))
1363+
.findFirst()
1364+
.orElseThrow(() -> new AssertionError("Domain not found in list"));
1365+
Votes votes = listed.getVotes();
1366+
boolean voterInUpVotes =
1367+
votes != null
1368+
&& votes.getUpVoters() != null
1369+
&& votes.getUpVoters().stream()
1370+
.anyMatch(ref -> ref != null && voter.getId().equals(ref.getId()));
1371+
assertFalse(voterInUpVotes, "Soft-deleted voter must not appear in list endpoint votes");
1372+
}
11671373
}

0 commit comments

Comments
 (0)