@@ -14,6 +14,7 @@ const mockUpdateSingleAbacAttributeValuesById = jest.fn();
1414const mockUpdateAbacAttributeValuesArrayFilteredById = jest . fn ( ) ;
1515const mockRemoveAbacAttributeByRoomIdAndKey = jest . fn ( ) ;
1616const mockInsertAbacAttributeIfNotExistsById = jest . fn ( ) ;
17+ const mockUsersFind = jest . fn ( ) ;
1718
1819jest . mock ( '@rocket.chat/models' , ( ) => ( {
1920 Rooms : {
@@ -37,12 +38,22 @@ jest.mock('@rocket.chat/models', () => ({
3738 removeById : ( ...args : any [ ] ) => mockAbacDeleteOne ( ...args ) ,
3839 find : ( ...args : any [ ] ) => mockAbacFind ( ...args ) ,
3940 } ,
41+ Users : {
42+ find : ( ...args : any [ ] ) => mockUsersFind ( ...args ) ,
43+ } ,
4044} ) ) ;
4145
42- // Minimal mock for ServiceClass (we don't need its real behavior in unit scope)
43- jest . mock ( '@rocket.chat/core-services' , ( ) => ( {
44- ServiceClass : class { } ,
45- } ) ) ;
46+ // Partial mock for @rocket .chat/core-services: keep real MeteorError, override ServiceClass and Room
47+ jest . mock ( '@rocket.chat/core-services' , ( ) => {
48+ const actual = jest . requireActual ( '@rocket.chat/core-services' ) ;
49+ return {
50+ ...actual ,
51+ ServiceClass : class { } ,
52+ Room : {
53+ removeUserFromRoom : jest . fn ( ) ,
54+ } ,
55+ } ;
56+ } ) ;
4657
4758describe ( 'AbacService (unit)' , ( ) => {
4859 let service : AbacService ;
@@ -815,4 +826,121 @@ describe('AbacService (unit)', () => {
815826 } ) ;
816827 } ) ;
817828 } ) ;
829+
830+ describe ( 'checkUsernamesMatchAttributes' , ( ) => {
831+ beforeEach ( ( ) => {
832+ mockUsersFind . mockReset ( ) ;
833+ } ) ;
834+
835+ const attributes = [ { key : 'dept' , values : [ 'eng' ] } ] ;
836+
837+ it ( 'returns early (no query) when usernames array is empty' , async ( ) => {
838+ await expect ( service . checkUsernamesMatchAttributes ( [ ] , attributes as any ) ) . resolves . toBeUndefined ( ) ;
839+ expect ( mockUsersFind ) . not . toHaveBeenCalled ( ) ;
840+ } ) ;
841+
842+ it ( 'returns early (no query) when attributes array is empty' , async ( ) => {
843+ await expect ( service . checkUsernamesMatchAttributes ( [ 'alice' ] , [ ] ) ) . resolves . toBeUndefined ( ) ;
844+ expect ( mockUsersFind ) . not . toHaveBeenCalled ( ) ;
845+ } ) ;
846+
847+ it ( 'resolves when all provided usernames are compliant (query returns empty)' , async ( ) => {
848+ const usernames = [ 'alice' , 'bob' ] ;
849+ mockUsersFind . mockImplementationOnce ( ( ) => ( {
850+ map : ( ) => ( {
851+ toArray : async ( ) => [ ] ,
852+ } ) ,
853+ } ) ) ;
854+
855+ await expect ( service . checkUsernamesMatchAttributes ( usernames , attributes as any ) ) . resolves . toBeUndefined ( ) ;
856+
857+ expect ( mockUsersFind ) . toHaveBeenCalledWith (
858+ {
859+ username : { $in : usernames } ,
860+ $or : [
861+ {
862+ abacAttributes : {
863+ $not : {
864+ $elemMatch : {
865+ key : 'dept' ,
866+ values : { $all : [ 'eng' ] } ,
867+ } ,
868+ } ,
869+ } ,
870+ } ,
871+ ] ,
872+ } ,
873+ { projection : { username : 1 } } ,
874+ ) ;
875+ } ) ;
876+
877+ it ( 'rejects with error-usernames-not-matching-abac-attributes and details for non-compliant users' , async ( ) => {
878+ const usernames = [ 'alice' , 'bob' , 'charlie' ] ;
879+ const nonCompliantDocs = [ { username : 'bob' } , { username : 'charlie' } ] ;
880+ mockUsersFind . mockImplementationOnce ( ( ) => ( {
881+ map : ( fn : ( u : any ) => string ) => ( {
882+ toArray : async ( ) => nonCompliantDocs . map ( fn ) ,
883+ } ) ,
884+ } ) ) ;
885+
886+ await expect ( service . checkUsernamesMatchAttributes ( usernames , attributes as any ) ) . rejects . toMatchObject ( {
887+ error : 'error-usernames-not-matching-abac-attributes' ,
888+ message : expect . stringContaining ( '[error-usernames-not-matching-abac-attributes]' ) ,
889+ details : expect . arrayContaining ( [ 'bob' , 'charlie' ] ) ,
890+ } ) ;
891+ } ) ;
892+ } ) ;
893+ describe ( 'buildNonCompliantConditions (private)' , ( ) => {
894+ it ( 'returns empty array for empty attributes list' , ( ) => {
895+ const result = ( service as any ) . buildNonCompliantConditions ( [ ] ) ;
896+ expect ( result ) . toEqual ( [ ] ) ;
897+ } ) ;
898+
899+ it ( 'maps single attribute to $not $elemMatch query' , ( ) => {
900+ const attrs = [ { key : 'dept' , values : [ 'eng' , 'sales' ] } ] ;
901+ const result = ( service as any ) . buildNonCompliantConditions ( attrs ) ;
902+ expect ( result ) . toEqual ( [
903+ {
904+ abacAttributes : {
905+ $not : {
906+ $elemMatch : {
907+ key : 'dept' ,
908+ values : { $all : [ 'eng' , 'sales' ] } ,
909+ } ,
910+ } ,
911+ } ,
912+ } ,
913+ ] ) ;
914+ } ) ;
915+
916+ it ( 'maps multiple attributes preserving order' , ( ) => {
917+ const attrs = [
918+ { key : 'dept' , values : [ 'eng' ] } ,
919+ { key : 'region' , values : [ 'emea' , 'apac' ] } ,
920+ ] ;
921+ const result = ( service as any ) . buildNonCompliantConditions ( attrs ) ;
922+ expect ( result ) . toEqual ( [
923+ {
924+ abacAttributes : {
925+ $not : {
926+ $elemMatch : {
927+ key : 'dept' ,
928+ values : { $all : [ 'eng' ] } ,
929+ } ,
930+ } ,
931+ } ,
932+ } ,
933+ {
934+ abacAttributes : {
935+ $not : {
936+ $elemMatch : {
937+ key : 'region' ,
938+ values : { $all : [ 'emea' , 'apac' ] } ,
939+ } ,
940+ } ,
941+ } ,
942+ } ,
943+ ] ) ;
944+ } ) ;
945+ } ) ;
818946} ) ;
0 commit comments