Skip to content

Commit d8fe3d4

Browse files
Update: Duplicate users and intermittent search results
Update UserServiceTest.java
1 parent f42464c commit d8fe3d4

File tree

2 files changed

+155
-135
lines changed

2 files changed

+155
-135
lines changed

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/UserServiceImpl.java

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,32 +1290,18 @@ public Page<UserEntity> search(ExecutionContext executionContext, String query,
12901290
// UserDocumentTransformation remove domain from email address for security reasons
12911291
// remove it during search phase to provide results
12921292
String sanitizedQuery = query.indexOf('@') > 0 ? query.substring(0, query.indexOf('@')) : query;
1293-
userQuery = QueryBuilder.create(UserEntity.class).setQuery(sanitizedQuery).setPage(pageable).build();
1293+
userQuery = QueryBuilder.create(UserEntity.class)
1294+
.setQuery(sanitizedQuery)
1295+
.setSort(new SortableImpl(UserDocumentTransformer.FIELD_LASTNAME_FIRSTNAME, true))
1296+
.setPage(pageable)
1297+
.build();
12941298
}
12951299
SearchResult results = searchEngineService.search(executionContext, userQuery);
12961300

12971301
if (results.hasResults()) {
1298-
Set<UserEntity> fetched = findByIds(executionContext, results.getDocuments());
1299-
Map<String, UserEntity> byId = fetched.stream().collect(Collectors.toMap(UserEntity::getId, u -> u));
1300-
1301-
List<UserEntity> users = new ArrayList<>(results.getDocuments().size());
1302-
Set<String> seen = new HashSet<>();
1303-
1304-
for (String id : results.getDocuments()) {
1305-
if (seen.add(id)) {
1306-
UserEntity u = byId.get(id);
1307-
if (u != null) {
1308-
users.add(u);
1309-
}
1310-
}
1311-
}
1312-
1313-
users.sort(
1314-
Comparator.comparing(UserEntity::getFirstname, Comparator.nullsLast(String::compareToIgnoreCase)).thenComparing(
1315-
UserEntity::getLastname,
1316-
Comparator.nullsLast(String::compareToIgnoreCase)
1317-
)
1318-
);
1302+
List<String> orderedIds = new ArrayList<>(results.getDocuments());
1303+
List<UserEntity> users = new ArrayList<>((findByIds(executionContext, orderedIds)));
1304+
users.sort(Comparator.comparingInt(user -> orderedIds.indexOf(user.getId())));
13191305

13201306
populateUserFlags(executionContext.getOrganizationId(), users);
13211307

@@ -1369,12 +1355,6 @@ public Page<UserEntity> search(ExecutionContext executionContext, UserCriteria c
13691355
.getContent()
13701356
.stream()
13711357
.map(u -> convert(u, false, userMetadataService.findAllByUserId(u.getId())))
1372-
.sorted(
1373-
Comparator.comparing(UserEntity::getFirstname, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)).thenComparing(
1374-
UserEntity::getLastname,
1375-
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)
1376-
)
1377-
)
13781358
.collect(toList());
13791359

13801360
populateUserFlags(executionContext.getOrganizationId(), entities);

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/UserServiceTest.java

Lines changed: 147 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,113 +2391,6 @@ private InputStream read(String resource) throws IOException {
23912391
return this.getClass().getResourceAsStream(resource);
23922392
}
23932393

2394-
@Test
2395-
public void shouldSearchUsers_hasResults_ordersUniqueAndPopulateFlags() {
2396-
UserServiceImpl spyUserService = spy(userService);
2397-
2398-
// Search engine returns duplicated ids and one unknown id
2399-
List<String> docs = Arrays.asList("u1", "u2", "u1", "u3", "u4", "u3");
2400-
io.gravitee.rest.api.service.impl.search.SearchResult searchResult = new io.gravitee.rest.api.service.impl.search.SearchResult(
2401-
docs,
2402-
42
2403-
);
2404-
when(searchEngineService.search(eq(EXECUTION_CONTEXT), any())).thenReturn(searchResult);
2405-
2406-
// Prepare fetched users (u4 is missing on purpose)
2407-
UserEntity ue1 = new UserEntity();
2408-
ue1.setId("u1");
2409-
UserEntity ue2 = new UserEntity();
2410-
ue2.setId("u2");
2411-
UserEntity ue3 = new UserEntity();
2412-
ue3.setId("u3");
2413-
doReturn(new HashSet<>(Arrays.asList(ue1, ue2, ue3))).when(spyUserService).findByIds(eq(EXECUTION_CONTEXT), anyCollection());
2414-
2415-
// Mock roles for Primary Owner checks
2416-
RoleEntity apiPORole = mockRoleEntity(RoleScope.API, "PRIMARY_OWNER");
2417-
RoleEntity appPORole = mockRoleEntity(RoleScope.APPLICATION, "PRIMARY_OWNER");
2418-
when(roleService.findPrimaryOwnerRoleByOrganization(ORGANIZATION, RoleScope.API)).thenReturn(apiPORole);
2419-
when(roleService.findPrimaryOwnerRoleByOrganization(ORGANIZATION, RoleScope.APPLICATION)).thenReturn(appPORole);
2420-
2421-
// Only u2 is primary owner (API). Others not.
2422-
when(
2423-
membershipService.getMembershipsByMemberAndReferenceAndRole(
2424-
eq(MembershipMemberType.USER),
2425-
eq("u2"),
2426-
eq(MembershipReferenceType.API),
2427-
eq(apiPORole.getId())
2428-
)
2429-
).thenReturn(
2430-
java.util.Collections.<io.gravitee.rest.api.model.MembershipEntity>singleton(new io.gravitee.rest.api.model.MembershipEntity())
2431-
);
2432-
when(
2433-
membershipService.getMembershipsByMemberAndReferenceAndRole(
2434-
eq(MembershipMemberType.USER),
2435-
eq("u1"),
2436-
eq(MembershipReferenceType.API),
2437-
eq(apiPORole.getId())
2438-
)
2439-
).thenReturn(java.util.Collections.<io.gravitee.rest.api.model.MembershipEntity>emptySet());
2440-
when(
2441-
membershipService.getMembershipsByMemberAndReferenceAndRole(
2442-
eq(MembershipMemberType.USER),
2443-
eq("u3"),
2444-
eq(MembershipReferenceType.API),
2445-
eq(apiPORole.getId())
2446-
)
2447-
).thenReturn(java.util.Collections.<io.gravitee.rest.api.model.MembershipEntity>emptySet());
2448-
2449-
// No application PO for anyone
2450-
when(
2451-
membershipService.getMembershipsByMemberAndReferenceAndRole(
2452-
eq(MembershipMemberType.USER),
2453-
anyString(),
2454-
eq(MembershipReferenceType.APPLICATION),
2455-
eq(appPORole.getId())
2456-
)
2457-
).thenReturn(java.util.Collections.<io.gravitee.rest.api.model.MembershipEntity>emptySet());
2458-
2459-
// Tokens per user: u1=1, u2=3, u3=0
2460-
when(tokenService.findByUser("u1")).thenReturn(Collections.singletonList(new io.gravitee.rest.api.model.TokenEntity()));
2461-
when(tokenService.findByUser("u2")).thenReturn(
2462-
Arrays.asList(
2463-
new io.gravitee.rest.api.model.TokenEntity(),
2464-
new io.gravitee.rest.api.model.TokenEntity(),
2465-
new io.gravitee.rest.api.model.TokenEntity()
2466-
)
2467-
);
2468-
when(tokenService.findByUser("u3")).thenReturn(Collections.emptyList());
2469-
2470-
io.gravitee.rest.api.model.common.Pageable pageable = new io.gravitee.rest.api.model.common.PageableImpl(2, 5);
2471-
2472-
io.gravitee.common.data.domain.Page<UserEntity> page = spyUserService.search(EXECUTION_CONTEXT, "john", pageable);
2473-
2474-
// Then: order preserved, duplicates removed, missing id filtered out
2475-
List<UserEntity> content = page.getContent();
2476-
assertEquals(3, content.size());
2477-
assertEquals("u1", content.get(0).getId());
2478-
assertEquals("u2", content.get(1).getId());
2479-
assertEquals("u3", content.get(2).getId());
2480-
2481-
// Page metadata
2482-
assertEquals(2, page.getPageNumber());
2483-
assertEquals(5, page.getPageElements());
2484-
assertEquals(42, page.getTotalElements());
2485-
2486-
// Flags populated
2487-
assertFalse(content.get(0).isPrimaryOwner());
2488-
assertTrue(content.get(1).isPrimaryOwner());
2489-
assertFalse(content.get(2).isPrimaryOwner());
2490-
2491-
assertEquals(1, content.get(0).getNbActiveTokens());
2492-
assertEquals(3, content.get(1).getNbActiveTokens());
2493-
assertEquals(0, content.get(2).getNbActiveTokens());
2494-
2495-
// Verify that search was called and populateUserFlags implied calls
2496-
verify(searchEngineService).search(eq(EXECUTION_CONTEXT), any());
2497-
verify(roleService).findPrimaryOwnerRoleByOrganization(ORGANIZATION, RoleScope.API);
2498-
verify(roleService).findPrimaryOwnerRoleByOrganization(ORGANIZATION, RoleScope.APPLICATION);
2499-
}
2500-
25012394
@Test
25022395
public void shouldOverrideAdminRolesWithIdpMappingsWhenSyncMappingsEnabled() throws Exception {
25032396
reset(identityProvider, userRepository, roleService, membershipService);
@@ -2530,4 +2423,151 @@ public void shouldOverrideAdminRolesWithIdpMappingsWhenSyncMappingsEnabled() thr
25302423
eq("oauth2")
25312424
);
25322425
}
2426+
2427+
private User buildUser(String id) {
2428+
User u = new User();
2429+
u.setId(id);
2430+
u.setOrganizationId(ORGANIZATION);
2431+
u.setFirstname(null);
2432+
u.setLastname(null);
2433+
String email = id + "@example.com";
2434+
u.setEmail(email);
2435+
u.setSource("gravitee");
2436+
u.setSourceId(email);
2437+
u.setStatus(UserStatus.ACTIVE);
2438+
u.setLoginCount(0L);
2439+
Date fixedDate = new Date(1765179754234L);
2440+
u.setCreatedAt(fixedDate);
2441+
u.setUpdatedAt(fixedDate);
2442+
return u;
2443+
}
2444+
2445+
private void mockPopulateUserFlagsNoop() {
2446+
RoleEntity apiPoRole = new RoleEntity();
2447+
apiPoRole.setId("api-po");
2448+
when(roleService.findPrimaryOwnerRoleByOrganization(ORGANIZATION, RoleScope.API)).thenReturn(apiPoRole);
2449+
RoleEntity appPoRole = new RoleEntity();
2450+
appPoRole.setId("app-po");
2451+
when(roleService.findPrimaryOwnerRoleByOrganization(ORGANIZATION, RoleScope.APPLICATION)).thenReturn(appPoRole);
2452+
when(membershipService.getMembershipsByMemberAndReferenceAndRole(any(), anyString(), any(), anyString())).thenReturn(
2453+
Collections.emptySet()
2454+
);
2455+
when(tokenService.findByUser(anyString())).thenReturn(Collections.emptyList());
2456+
}
2457+
2458+
@Test
2459+
public void shouldSearchUsers_removeDuplicatesWithinPage_andPreserveOrder() throws TechnicalException {
2460+
mockPopulateUserFlagsNoop();
2461+
List<String> docs = Arrays.asList("u1", "u2", "u1", "u3");
2462+
when(searchEngineService.search(eq(EXECUTION_CONTEXT), any())).thenReturn(
2463+
new io.gravitee.rest.api.service.impl.search.SearchResult(docs, docs.size())
2464+
);
2465+
2466+
User u1 = buildUser("u1");
2467+
User u2 = buildUser("u2");
2468+
User u3 = buildUser("u3");
2469+
when(userRepository.findByIds(anyCollection())).thenReturn(new HashSet<>(Arrays.asList(u1, u2, u3)));
2470+
when(userMetadataService.findAllByUserId(anyString())).thenReturn(Collections.emptyList());
2471+
2472+
var page = userService.search(EXECUTION_CONTEXT, "john", new io.gravitee.rest.api.model.common.PageableImpl(1, 10));
2473+
2474+
List<UserEntity> content = page.getContent();
2475+
assertEquals(3, content.size());
2476+
assertEquals("u1", content.get(0).getId());
2477+
assertEquals("u2", content.get(1).getId());
2478+
assertEquals("u3", content.get(2).getId());
2479+
2480+
UserEntity first = content.get(0);
2481+
assertEquals(ORGANIZATION, first.getOrganizationId());
2482+
assertEquals("u1@example.com", first.getEmail());
2483+
assertEquals("gravitee", first.getSource());
2484+
assertEquals("u1@example.com", first.getSourceId());
2485+
assertEquals("ACTIVE", first.getStatus());
2486+
assertEquals(0L, first.getLoginCount());
2487+
assertEquals("u1@example.com", first.getDisplayName());
2488+
assertNotNull(first.getCreatedAt());
2489+
assertNotNull(first.getUpdatedAt());
2490+
assertFalse(first.isPrimaryOwner());
2491+
assertEquals(0, first.getNbActiveTokens());
2492+
}
2493+
2494+
@Test
2495+
public void shouldSearchUsers_returnConsistentResults_onRepeatedSameQuery() throws TechnicalException {
2496+
mockPopulateUserFlagsNoop();
2497+
List<String> docs = Arrays.asList("a", "b", "c");
2498+
when(searchEngineService.search(eq(EXECUTION_CONTEXT), any())).thenReturn(
2499+
new io.gravitee.rest.api.service.impl.search.SearchResult(docs, docs.size())
2500+
);
2501+
2502+
User a = buildUser("a");
2503+
User b = buildUser("b");
2504+
User c = buildUser("c");
2505+
when(userRepository.findByIds(anyCollection())).thenReturn(new HashSet<>(Arrays.asList(a, b, c)));
2506+
when(userMetadataService.findAllByUserId(anyString())).thenReturn(Collections.emptyList());
2507+
2508+
var page1 = userService.search(EXECUTION_CONTEXT, "same-query", new io.gravitee.rest.api.model.common.PageableImpl(1, 3));
2509+
var page2 = userService.search(EXECUTION_CONTEXT, "same-query", new io.gravitee.rest.api.model.common.PageableImpl(1, 3));
2510+
2511+
assertEquals(page1.getTotalElements(), page2.getTotalElements());
2512+
assertEquals(page1.getContent().size(), page2.getContent().size());
2513+
for (int i = 0; i < page1.getContent().size(); i++) {
2514+
assertEquals(page1.getContent().get(i).getId(), page2.getContent().get(i).getId());
2515+
}
2516+
2517+
UserEntity first = page1.getContent().get(0);
2518+
assertEquals(ORGANIZATION, first.getOrganizationId());
2519+
assertEquals("a@example.com", first.getEmail());
2520+
assertEquals("gravitee", first.getSource());
2521+
assertEquals("a@example.com", first.getSourceId());
2522+
assertEquals("ACTIVE", first.getStatus());
2523+
assertEquals(0L, first.getLoginCount());
2524+
assertEquals("a@example.com", first.getDisplayName());
2525+
assertNotNull(first.getCreatedAt());
2526+
assertNotNull(first.getUpdatedAt());
2527+
assertFalse(first.isPrimaryOwner());
2528+
assertEquals(0, first.getNbActiveTokens());
2529+
}
2530+
2531+
@Test
2532+
public void shouldSearchUsers_handleDuplicateAcrossPages_consistently() throws TechnicalException {
2533+
mockPopulateUserFlagsNoop();
2534+
List<String> docsPage1 = Arrays.asList("x1", "x2", "x3");
2535+
List<String> docsPage2 = Arrays.asList("x3", "x4", "x5"); // overlap with page1 on x3
2536+
2537+
User x1 = buildUser("x1");
2538+
User x2 = buildUser("x2");
2539+
User x3 = buildUser("x3");
2540+
User x4 = buildUser("x4");
2541+
User x5 = buildUser("x5");
2542+
2543+
Map<String, User> registry = new HashMap<>();
2544+
registry.put("x1", x1);
2545+
registry.put("x2", x2);
2546+
registry.put("x3", x3);
2547+
registry.put("x4", x4);
2548+
registry.put("x5", x5);
2549+
when(userRepository.findByIds(anyCollection())).thenAnswer(invocation -> {
2550+
@SuppressWarnings("unchecked")
2551+
Collection<String> ids = (Collection<String>) invocation.getArgument(0);
2552+
Set<User> result = new HashSet<>();
2553+
if (ids != null) {
2554+
for (String id : ids) {
2555+
User u = registry.get(id);
2556+
if (u != null) result.add(u);
2557+
}
2558+
}
2559+
return result;
2560+
});
2561+
when(userMetadataService.findAllByUserId(anyString())).thenReturn(Collections.emptyList());
2562+
2563+
when(searchEngineService.search(eq(EXECUTION_CONTEXT), any()))
2564+
.thenReturn(new io.gravitee.rest.api.service.impl.search.SearchResult(docsPage1, 5L))
2565+
.thenReturn(new io.gravitee.rest.api.service.impl.search.SearchResult(docsPage2, 5L));
2566+
2567+
var page1 = userService.search(EXECUTION_CONTEXT, "overlap", new io.gravitee.rest.api.model.common.PageableImpl(1, 3));
2568+
var page2 = userService.search(EXECUTION_CONTEXT, "overlap", new io.gravitee.rest.api.model.common.PageableImpl(2, 3));
2569+
2570+
assertEquals(List.of("x1", "x2", "x3"), page1.getContent().stream().map(UserEntity::getId).toList());
2571+
assertEquals(List.of("x3", "x4", "x5"), page2.getContent().stream().map(UserEntity::getId).toList());
2572+
}
25332573
}

0 commit comments

Comments
 (0)