@@ -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