From 24cddf502aafe7954f29825aca593c9f98cd1012 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 30 Dec 2025 07:53:54 -0600 Subject: [PATCH 1/9] flaky --- .../abac/src/user-auto-removal.spec.ts | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index 4fae4f0c05c02..665f59467f399 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -156,15 +156,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { expect(auditedUsers).toEqual(['u2_newkey', 'u3_newkey']); expect(auditedRooms).toEqual(new Set([rid1])); expect(auditedActions).toEqual(new Set(['room-attributes-change'])); - - const remaining = await usersCol - .find({ _id: { $in: ['u1_newkey', 'u2_newkey', 'u3_newkey', 'u4_newkey'] } }, { projection: { __rooms: 1 } }) - .toArray() - .then((docs) => Object.fromEntries(docs.map((d) => [d._id, d.__rooms || []]))); - expect(remaining.u1_newkey).toContain(rid1); - expect(remaining.u4_newkey).toContain(rid1); - expect(remaining.u2_newkey).not.toContain(rid1); - expect(remaining.u3_newkey).not.toContain(rid1); }); it('handles duplicate values in room attributes equivalently to unique set (logs non compliant and removes them)', async () => { @@ -206,14 +197,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { expect(auditSpy.mock.calls[0][0]).toMatchObject({ _id: 'u2_newval', username: 'u2_newval' }); expect(auditSpy.mock.calls[0][1]).toMatchObject({ _id: rid }); expect(auditSpy.mock.calls[0][2]).toBe('room-attributes-change'); - - const users = await usersCol - .find({ _id: { $in: ['u1_newval', 'u2_newval', 'u3_newval'] } }, { projection: { __rooms: 1 } }) - .toArray() - .then((docs) => Object.fromEntries(docs.map((d) => [d._id, d.__rooms || []]))); - expect(users.u1_newval).toContain(rid); - expect(users.u3_newval).toContain(rid); - expect(users.u2_newval).not.toContain(rid); }); it('produces no evaluation log when only removing values from existing attribute', async () => { @@ -311,16 +294,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { expect(auditedRooms).toEqual(new Set([rid])); const auditedActions = new Set(auditSpy.mock.calls.map((call) => call[2])); expect(auditedActions).toEqual(new Set(['room-attributes-change'])); - - const memberships = await usersCol - .find({ _id: { $in: ['u1_multi', 'u2_multi', 'u3_multi', 'u4_multi', 'u5_multi'] } }, { projection: { __rooms: 1 } }) - .toArray() - .then((docs) => Object.fromEntries(docs.map((d) => [d._id, d.__rooms || []]))); - expect(memberships.u1_multi).toContain(rid); - expect(memberships.u4_multi).toContain(rid); - expect(memberships.u2_multi).not.toContain(rid); - expect(memberships.u3_multi).not.toContain(rid); - expect(memberships.u5_multi).not.toContain(rid); }); }); @@ -421,14 +394,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { expect(auditedRooms).toEqual(new Set([ridMissingKey])); const auditedActions = new Set(auditSpy.mock.calls.map((call) => call[2])); expect(auditedActions).toEqual(new Set(['room-attributes-change'])); - - const memberships = await usersCol - .find({ _id: { $in: ['u1_misskey', 'u2_misskey', 'u3_misskey'] } }, { projection: { __rooms: 1 } }) - .toArray() - .then((docs) => Object.fromEntries(docs.map((d) => [d._id, d.__rooms || []]))); - expect(memberships.u1_misskey).toContain(ridMissingKey); - expect(memberships.u2_misskey).not.toContain(ridMissingKey); - expect(memberships.u3_misskey).not.toContain(ridMissingKey); }); }); @@ -469,9 +434,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { expect(auditedRooms).toEqual(new Set([rid])); const auditedActions = new Set(auditSpy.mock.calls.map((call) => call[2])); expect(auditedActions).toEqual(new Set(['room-attributes-change'])); - - const remainingCount = await usersCol.countDocuments({ __rooms: rid }); - expect(remainingCount).toBe(150); }); }); }); From af8699452de6bf39f5710b97e5ebdc326378f973 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 30 Dec 2025 09:20:19 -0600 Subject: [PATCH 2/9] fix --- .../abac/src/subject-attributes-validations.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index dbc9e9b157c05..fd2186b4c6fde 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -234,13 +234,16 @@ describe('Subject Attributes validation', () => { __rooms: user.__rooms || [], }); - beforeEach(async () => { - service = new AbacService(); + beforeAll(async () => { roomsCol = db.collection('rocketchat_room'); usersCol = db.collection('users'); await Promise.all([roomsCol.deleteMany({}), usersCol.deleteMany({})]); }); + beforeEach(() => { + service = new AbacService(); + }); + it('removes user from rooms whose attributes become non-compliant after losing a value', async () => { const user: IUser = { _id: 'u-loss', From 58bcc9acc3bd75ca2aa4863c35899081b782805f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 30 Dec 2025 10:29:24 -0600 Subject: [PATCH 3/9] test --- .../subject-attributes-validations.spec.ts | 9 ++++++-- .../abac/src/user-auto-removal.spec.ts | 22 +++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index fd2186b4c6fde..789833ac0dd4a 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -1,5 +1,5 @@ import type { ILDAPEntry, IUser, IAbacAttributeDefinition } from '@rocket.chat/core-typings'; -import { registerServiceModels, Users } from '@rocket.chat/models'; +import { registerModel, Users, UsersRaw, RoomsRaw, AbacAttributesRaw, ServerEventsRaw } from '@rocket.chat/models'; import type { Collection, Db } from 'mongodb'; import { MongoClient } from 'mongodb'; import { MongoMemoryServer } from 'mongodb-memory-server'; @@ -40,7 +40,12 @@ describe('Subject Attributes validation', () => { mongo = await MongoMemoryServer.create(); client = await MongoClient.connect(mongo.getUri(), {}); db = client.db('abac_global'); - registerServiceModels(db); + + // Register only the models we actually need for these tests + registerModel('IUsersModel', () => new UsersRaw(db)); + registerModel('IRoomsModel', () => new RoomsRaw(db)); + registerModel('IAbacAttributesModel', () => new AbacAttributesRaw(db)); + registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); // @ts-expect-error - ignore await db.collection('abac_dummy_init').insertOne({ _id: 'init', createdAt: new Date() }); diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index 665f59467f399..8019cab7adcf8 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -1,5 +1,13 @@ import type { IAbacAttributeDefinition, IRoom, IUser } from '@rocket.chat/core-typings'; -import { registerServiceModels } from '@rocket.chat/models'; +import { + registerModel, + Subscriptions, + SubscriptionsRaw, + UsersRaw, + RoomsRaw, + AbacAttributesRaw, + ServerEventsRaw, +} from '@rocket.chat/models'; import type { Collection, Db } from 'mongodb'; import { MongoClient } from 'mongodb'; import { MongoMemoryServer } from 'mongodb-memory-server'; @@ -12,7 +20,6 @@ jest.mock('@rocket.chat/core-services', () => ({ Room: { // Mimic the DB side-effects of removing a user from a room (no apps/system messages) removeUserFromRoom: async (roomId: string, user: any) => { - const { Subscriptions } = await import('@rocket.chat/models'); await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); }, }, @@ -85,11 +92,12 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { client = await MongoClient.connect(mongo.getUri(), {}); db = client.db('abac_integration'); - // Hack to register the models in here with a custom database without having to call every model by one - registerServiceModels(db as any); - - // @ts-expect-error - ignore - await db.collection('abac_dummy_init').insertOne({ _id: 'init', createdAt: new Date() }); + // Register only the models we actually need for these tests + registerModel('IUsersModel', () => new UsersRaw(db)); + registerModel('IRoomsModel', () => new RoomsRaw(db)); + registerModel('IAbacAttributesModel', () => new AbacAttributesRaw(db)); + registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); + registerModel('ISubscriptionsModel', () => new SubscriptionsRaw(db)); service = new AbacService(); debugSpy = jest.spyOn((service as any).logger, 'debug').mockImplementation(() => undefined); From d1db9e0fbaa7dc05c88c943eb585bf1a322ebad5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 30 Dec 2025 13:52:47 -0600 Subject: [PATCH 4/9] cr --- ee/packages/abac/src/subject-attributes-validations.spec.ts | 6 +----- ee/packages/abac/src/user-auto-removal.spec.ts | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index 789833ac0dd4a..825a8cad31976 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -57,11 +57,7 @@ describe('Subject Attributes validation', () => { }); describe('AbacService.addSubjectAttributes (unit)', () => { - let service: AbacService; - - beforeEach(async () => { - service = new AbacService(); - }); + const service = new AbacService(); describe('early returns and no-ops', () => { it('returns early when user has no _id', async () => { diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index 8019cab7adcf8..f62daf0dffe1f 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -29,7 +29,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { let mongo: MongoMemoryServer; let client: MongoClient; let db: Db; - let service: AbacService; + const service = new AbacService(); let roomsCol: Collection; let usersCol: Collection; @@ -99,7 +99,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); registerModel('ISubscriptionsModel', () => new SubscriptionsRaw(db)); - service = new AbacService(); debugSpy = jest.spyOn((service as any).logger, 'debug').mockImplementation(() => undefined); auditSpy = jest.spyOn(Audit, 'actionPerformed').mockResolvedValue(); From 30729abc52c2e9cd469a1f92ec1d3d41e529ee09 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 5 Jan 2026 11:16:48 -0600 Subject: [PATCH 5/9] more fixes --- .../subject-attributes-validations.spec.ts | 172 +++++----- .../abac/src/user-auto-removal.spec.ts | 313 ++++++++++-------- 2 files changed, 276 insertions(+), 209 deletions(-) diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index 825a8cad31976..26640ee646551 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -16,13 +16,13 @@ jest.mock('@rocket.chat/core-services', () => ({ const makeUser = (overrides: Partial = {}): IUser => ({ - _id: `user-${Math.random().toString(36).substring(2, 15)}`, - username: `user${Math.random().toString(36).substring(2, 15)}`, + _id: `user-fixed-id-${Math.random()}`, + username: 'user-fixed-username', roles: [], type: 'user', active: true, - createdAt: new Date(), - _updatedAt: new Date(), + createdAt: new Date(0), + _updatedAt: new Date(0), ...overrides, }) as IUser; @@ -35,6 +35,21 @@ describe('Subject Attributes validation', () => { let db: Db; let mongo: MongoMemoryServer; let client: MongoClient; + const service = new AbacService(); + + let roomsCol: Collection; + let usersCol: Collection; + + const insertRooms = async (rooms: { _id: string; abacAttributes?: IAbacAttributeDefinition[] }[]) => { + await roomsCol.insertMany( + rooms.map((room) => ({ + _id: room._id, + name: room._id, + t: 'p', + abacAttributes: room.abacAttributes, + })), + ); + }; beforeAll(async () => { mongo = await MongoMemoryServer.create(); @@ -48,7 +63,10 @@ describe('Subject Attributes validation', () => { registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); // @ts-expect-error - ignore - await db.collection('abac_dummy_init').insertOne({ _id: 'init', createdAt: new Date() }); + await db.collection('abac_dummy_init').insertOne({ _id: 'init', createdAt: new Date(0) }); + + roomsCol = db.collection('rocketchat_room'); + usersCol = db.collection('users'); }, 30_000); afterAll(async () => { @@ -56,9 +74,16 @@ describe('Subject Attributes validation', () => { await mongo.stop(); }); - describe('AbacService.addSubjectAttributes (unit)', () => { - const service = new AbacService(); + beforeEach(async () => { + // Clear state between tests while reusing the same in-memory DB & models + await Promise.all([usersCol.deleteMany({}), roomsCol.deleteMany({})]); + }); + + const insertUser = async (user: IUser) => { + await usersCol.insertOne(user); + }; + describe('AbacService.addSubjectAttributes (unit)', () => { describe('early returns and no-ops', () => { it('returns early when user has no _id', async () => { const user = makeUser({ _id: undefined }); @@ -70,7 +95,7 @@ describe('Subject Attributes validation', () => { it('does nothing (no update) when map produces no attributes and user had none', async () => { const user = makeUser({ abacAttributes: undefined }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ group: '' }); await service.addSubjectAttributes(user, ldap, { group: 'dept' }); const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); @@ -82,7 +107,7 @@ describe('Subject Attributes validation', () => { describe('building and setting attributes', () => { it('merges multiple LDAP keys mapping to the same ABAC key, deduplicating values', async () => { const user = makeUser(); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ memberOf: ['eng', 'sales', 'eng'], department: ['sales', 'support'], @@ -95,7 +120,7 @@ describe('Subject Attributes validation', () => { it('creates distinct ABAC attributes for different mapped keys preserving insertion order', async () => { const user = makeUser(); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ groups: ['alpha', 'beta'], regionCodes: ['emea', 'apac'], @@ -113,7 +138,7 @@ describe('Subject Attributes validation', () => { it('merges array and string LDAP values into one attribute', async () => { const user = makeUser(); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ deptCode: 'eng', deptName: ['engineering', 'eng'] }); const map = { deptCode: 'dept', deptName: 'dept' }; await service.addSubjectAttributes(user, ldap, map); @@ -125,7 +150,7 @@ describe('Subject Attributes validation', () => { describe('unsetting attributes when none extracted', () => { it('unsets abacAttributes when user previously had attributes but now extracts none', async () => { const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ other: ['x'] }); const map = { missing: 'dept' }; await service.addSubjectAttributes(user, ldap, map); @@ -135,7 +160,7 @@ describe('Subject Attributes validation', () => { it('does not unset when user had no prior attributes and extraction yields none', async () => { const user = makeUser({ abacAttributes: [] }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({}); const map = { missing: 'dept' }; await service.addSubjectAttributes(user, ldap, map); @@ -147,7 +172,7 @@ describe('Subject Attributes validation', () => { describe('loss detection triggering hook (attribute changes)', () => { it('updates attributes reducing values on loss', async () => { const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ memberOf: ['eng'] }); await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); @@ -161,7 +186,7 @@ describe('Subject Attributes validation', () => { { key: 'region', values: ['emea'] }, ], }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ department: ['eng'] }); await service.addSubjectAttributes(user, ldap, { department: 'dept' }); const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); @@ -170,7 +195,7 @@ describe('Subject Attributes validation', () => { it('gains new values without triggering loss logic', async () => { const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng'] }] }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ memberOf: ['eng', 'qa'] }); await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); @@ -179,7 +204,7 @@ describe('Subject Attributes validation', () => { it('keeps attributes unchanged when only ordering differs', async () => { const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ memberOf: ['qa', 'eng'] }); await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); @@ -189,7 +214,7 @@ describe('Subject Attributes validation', () => { it('merges duplicate LDAP mapping keys retaining union of values', async () => { const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }); - await Users.insertOne(user); + await insertUser(user); const ldap = makeLdap({ deptA: ['eng', 'sales'], deptB: ['eng'] }); await service.addSubjectAttributes(user, ldap, { deptA: 'dept', deptB: 'dept' }); const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); @@ -198,53 +223,42 @@ describe('Subject Attributes validation', () => { }); describe('input immutability', () => { + let sharedUser: IUser; + let original: IAbacAttributeDefinition[]; + let clone: IAbacAttributeDefinition[]; + + beforeAll(async () => { + original = [{ key: 'dept', values: ['eng', 'sales'] }] as IAbacAttributeDefinition[]; + clone = JSON.parse(JSON.stringify(original)); + sharedUser = makeUser({ abacAttributes: original }); + await insertUser(sharedUser); + }); + + afterAll(async () => { + await usersCol.deleteOne({ _id: sharedUser._id }); + }); + it('does not mutate original user.abacAttributes array reference contents', async () => { - const original = [{ key: 'dept', values: ['eng', 'sales'] }] as IAbacAttributeDefinition[]; - const user = makeUser({ abacAttributes: original }); - await Users.insertOne(user); - const clone = JSON.parse(JSON.stringify(original)); const ldap = makeLdap({ memberOf: ['eng', 'sales', 'support'] }); - await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); + await service.addSubjectAttributes(sharedUser, ldap, { memberOf: 'dept' }); expect(original).toEqual(clone); }); }); }); describe('AbacService.addSubjectAttributes (room removals)', () => { - let service: AbacService; - let roomsCol: Collection; - let usersCol: Collection; - const originalCoreServices = jest.requireMock('@rocket.chat/core-services'); originalCoreServices.Room.removeUserFromRoom = async (rid: string, user: IUser) => { // @ts-expect-error - test await usersCol.updateOne({ _id: user._id }, { $pull: { __rooms: rid } }); }; - const insertRoom = async (room: { _id: string; abacAttributes?: IAbacAttributeDefinition[] }) => - roomsCol.insertOne({ - _id: room._id, - name: room._id, - t: 'p', - abacAttributes: room.abacAttributes, - }); - - const insertUser = async (user: IUser & { __rooms?: string[] }) => + const insertUserForRemovalTests = async (user: IUser & { __rooms?: string[] }) => usersCol.insertOne({ ...user, __rooms: user.__rooms || [], }); - beforeAll(async () => { - roomsCol = db.collection('rocketchat_room'); - usersCol = db.collection('users'); - await Promise.all([roomsCol.deleteMany({}), usersCol.deleteMany({})]); - }); - - beforeEach(() => { - service = new AbacService(); - }); - it('removes user from rooms whose attributes become non-compliant after losing a value', async () => { const user: IUser = { _id: 'u-loss', @@ -261,12 +275,12 @@ describe('Subject Attributes validation', () => { // Rooms: // rKeep requires only 'eng' (will remain compliant) // rRemove requires both 'eng' and 'qa' (will become non-compliant after loss) - await Promise.all([ - insertRoom({ _id: 'rKeep', abacAttributes: [{ key: 'dept', values: ['eng'] }] }), - insertRoom({ _id: 'rRemove', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }), + await insertRooms([ + { _id: 'rKeep', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, + { _id: 'rRemove', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, ]); - await insertUser({ ...user, __rooms: ['rKeep', 'rRemove'] }); + await insertUserForRemovalTests({ ...user, __rooms: ['rKeep', 'rRemove'] }); const ldap: ILDAPEntry = { memberOf: ['eng'], @@ -301,19 +315,19 @@ describe('Subject Attributes validation', () => { // rDeptOnly -> only dept (will stay) // rRegionOnly -> region only (will be removed after region key loss) // rBoth -> both dept & region (will be removed) - await Promise.all([ - insertRoom({ _id: 'rDeptOnly', abacAttributes: [{ key: 'dept', values: ['eng'] }] }), - insertRoom({ _id: 'rRegionOnly', abacAttributes: [{ key: 'region', values: ['emea'] }] }), - insertRoom({ + await insertRooms([ + { _id: 'rDeptOnly', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, + { _id: 'rRegionOnly', abacAttributes: [{ key: 'region', values: ['emea'] }] }, + { _id: 'rBoth', abacAttributes: [ { key: 'dept', values: ['eng'] }, { key: 'region', values: ['emea'] }, ], - }), + }, ]); - await insertUser({ ...user, __rooms: ['rDeptOnly', 'rRegionOnly', 'rBoth'] }); + await insertUserForRemovalTests({ ...user, __rooms: ['rDeptOnly', 'rRegionOnly', 'rBoth'] }); const ldap: ILDAPEntry = { department: ['eng'], @@ -341,12 +355,12 @@ describe('Subject Attributes validation', () => { __rooms: ['rGrowthA', 'rGrowthB'], }; - await Promise.all([ - insertRoom({ _id: 'rGrowthA', abacAttributes: [{ key: 'dept', values: ['eng'] }] }), - insertRoom({ _id: 'rGrowthB', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }), // superset; still compliant after growth + await insertRooms([ + { _id: 'rGrowthA', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, + { _id: 'rGrowthB', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, // superset; still compliant after growth ]); - await insertUser({ ...user, __rooms: ['rGrowthA', 'rGrowthB'] }); + await insertUserForRemovalTests({ ...user, __rooms: ['rGrowthA', 'rGrowthB'] }); const ldap: ILDAPEntry = { memberOf: ['eng', 'qa'], @@ -376,21 +390,21 @@ describe('Subject Attributes validation', () => { __rooms: ['rExtraKeyRoom', 'rBaseline'], }; - await Promise.all([ - insertRoom({ + await insertRooms([ + { _id: 'rExtraKeyRoom', abacAttributes: [ { key: 'dept', values: ['eng', 'sales'] }, { key: 'project', values: ['X'] }, ], - }), - insertRoom({ + }, + { _id: 'rBaseline', abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }], - }), + }, ]); - await insertUser({ ...user, __rooms: ['rExtraKeyRoom', 'rBaseline'] }); + await insertUserForRemovalTests({ ...user, __rooms: ['rExtraKeyRoom', 'rBaseline'] }); const ldap: ILDAPEntry = { deptCodes: ['eng', 'sales'], @@ -417,12 +431,12 @@ describe('Subject Attributes validation', () => { __rooms: ['rAny1', 'rAny2'], }; - await Promise.all([ - insertRoom({ _id: 'rAny1', abacAttributes: [{ key: 'dept', values: ['eng'] }] }), - insertRoom({ _id: 'rAny2', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }), + await insertRooms([ + { _id: 'rAny1', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, + { _id: 'rAny2', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, ]); - await insertUser({ ...user, __rooms: ['rAny1', 'rAny2'] }); + await insertUserForRemovalTests({ ...user, __rooms: ['rAny1', 'rAny2'] }); const ldap: ILDAPEntry = { unrelated: ['x'], @@ -454,15 +468,17 @@ describe('Subject Attributes validation', () => { __rooms: ['rDeptRegion'], }; - await insertRoom({ - _id: 'rDeptRegion', - abacAttributes: [ - { key: 'dept', values: ['eng'] }, - { key: 'region', values: ['emea'] }, - ], - }); + await insertRooms([ + { + _id: 'rDeptRegion', + abacAttributes: [ + { key: 'dept', values: ['eng'] }, + { key: 'region', values: ['emea'] }, + ], + }, + ]); - await insertUser({ ...user, __rooms: ['rDeptRegion'] }); + await insertUserForRemovalTests({ ...user, __rooms: ['rDeptRegion'] }); const ldap: ILDAPEntry = { department: ['eng', 'ceo'], diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index f62daf0dffe1f..a0931fcd44113 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -37,10 +37,9 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { const fakeActor = { _id: 'test-user', username: 'testuser', type: 'user' }; const insertDefinitions = async (defs: { key: string; values: string[] }[]) => { - const svc = new AbacService(); await Promise.all( defs.map((def) => - svc.addAbacAttribute({ key: def.key, values: def.values }, fakeActor).catch((e: any) => { + service.addAbacAttribute({ key: def.key, values: def.values }, fakeActor).catch((e: any) => { if (e instanceof Error && e.message === 'error-duplicate-attribute-key') { return; } @@ -120,33 +119,35 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { let rid1: string; let rid2: string; - beforeAll(async () => { + beforeEach(async () => { rid1 = await insertRoom([]); - await Promise.all([ - insertDefinitions([{ key: 'dept', values: ['eng', 'sales', 'hr'] }]), - insertUsers([ - { _id: 'u1_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // compliant - { _id: 'u2_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing sales - { _id: 'u3_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'location', values: ['emea'] }] }, // missing dept key - { _id: 'u4_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng', 'sales', 'hr'] }] }, // superset - { _id: 'u5_newkey', member: false, abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // not in room - ]), - ]); - rid2 = await insertRoom([]); - await Promise.all([ - insertDefinitions([{ key: 'dep2t', values: ['eng', 'sales'] }]), - insertUsers([ - { _id: 'u1_dupvals', member: true, extraRooms: [rid2], abacAttributes: [{ key: 'dep2t', values: ['eng', 'sales'] }] }, - { _id: 'u2_dupvals', member: true, extraRooms: [rid2], abacAttributes: [{ key: 'dep2t', values: ['eng'] }] }, // non compliant (missing sales) - ]), + await insertDefinitions([ + { key: 'dept', values: ['eng', 'sales', 'hr'] }, + { key: 'dep2t', values: ['eng', 'sales'] }, + ]); + + await insertUsers([ + { _id: 'u1_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // compliant + { _id: 'u2_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing sales + { _id: 'u3_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'location', values: ['emea'] }] }, // missing dept key + { _id: 'u4_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng', 'sales', 'hr'] }] }, // superset + { _id: 'u5_newkey', member: false, abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // not in room + { _id: 'u1_dupvals', member: true, extraRooms: [rid2], abacAttributes: [{ key: 'dep2t', values: ['eng', 'sales'] }] }, + { _id: 'u2_dupvals', member: true, extraRooms: [rid2], abacAttributes: [{ key: 'dep2t', values: ['eng'] }] }, // non compliant (missing sales) ]); - }, 30_000); + }); - beforeEach(() => { - auditSpy.mockReset(); + afterEach(async () => { + await usersCol.deleteMany({ + _id: { + $in: ['u1_newkey', 'u2_newkey', 'u3_newkey', 'u4_newkey', 'u5_newkey', 'u1_dupvals', 'u2_dupvals'], + }, + }); + await roomsCol.deleteMany({ _id: { $in: [rid1, rid2] } }); }); + it('logs users that do not satisfy newly added attribute key or its values and actually removes them', async () => { const changeSpy = jest.spyOn(service as any, 'onRoomAttributesChanged'); @@ -182,107 +183,137 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }); describe('updateRoomAbacAttributeValues - new value addition', () => { - let rid: string; + describe('when adding new values', () => { + let rid: string; - beforeAll(async () => { - rid = await insertRoom([{ key: 'dept', values: ['eng'] }]); + beforeEach(async () => { + rid = await insertRoom([{ key: 'dept', values: ['eng'] }]); - await Promise.all([ - insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]), - insertUsers([ + await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); + await insertUsers([ { _id: 'u1_newval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // already superset { _id: 'u2_newval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing new value { _id: 'u3_newval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales', 'hr'] }] }, // superset - ]), - ]); - }, 30_000); + ]); + }); + + afterEach(async () => { + await usersCol.deleteMany({ + _id: { + $in: ['u1_newval', 'u2_newval', 'u3_newval'], + }, + }); + await roomsCol.deleteOne({ _id: rid }); + }); - it('logs users missing newly added value while retaining compliant ones and removes the missing ones', async () => { - await service.updateRoomAbacAttributeValues(rid, 'dept', ['eng', 'sales'], fakeActor); + it('logs users missing newly added value while retaining compliant ones and removes the missing ones', async () => { + await service.updateRoomAbacAttributeValues(rid, 'dept', ['eng', 'sales'], fakeActor); - expect(auditSpy).toHaveBeenCalledTimes(1); - expect(auditSpy.mock.calls[0][0]).toMatchObject({ _id: 'u2_newval', username: 'u2_newval' }); - expect(auditSpy.mock.calls[0][1]).toMatchObject({ _id: rid }); - expect(auditSpy.mock.calls[0][2]).toBe('room-attributes-change'); + expect(auditSpy).toHaveBeenCalledTimes(1); + expect(auditSpy.mock.calls[0][0]).toMatchObject({ _id: 'u2_newval', username: 'u2_newval' }); + expect(auditSpy.mock.calls[0][1]).toMatchObject({ _id: rid }); + expect(auditSpy.mock.calls[0][2]).toBe('room-attributes-change'); + }); }); - it('produces no evaluation log when only removing values from existing attribute', async () => { - const rid = await insertRoom([{ key: 'dept', values: ['eng', 'sales'] }]); + describe('when only removing values', () => { + let rid: string; - await Promise.all([ - insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]), - insertUsers([ + beforeEach(async () => { + rid = await insertRoom([{ key: 'dept', values: ['eng', 'sales'] }]); + + await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); + await insertUsers([ { _id: 'u1_rmval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, { _id: 'u2_rmval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, - ]), - ]); + ]); + }); + + afterEach(async () => { + await usersCol.deleteMany({ + _id: { + $in: ['u1_rmval', 'u2_rmval'], + }, + }); + await roomsCol.deleteOne({ _id: rid }); + }); - await service.updateRoomAbacAttributeValues(rid, 'dept', ['eng'], fakeActor); // removal only + it('produces no evaluation log when only removing values from existing attribute', async () => { + await service.updateRoomAbacAttributeValues(rid, 'dept', ['eng'], fakeActor); // removal only - expect(auditSpy).not.toHaveBeenCalled(); + expect(auditSpy).not.toHaveBeenCalled(); - // nobody removed because removal only does not trigger reevaluation - const u1 = await usersCol.findOne({ _id: 'u1_rmval' }, { projection: { __rooms: 1 } }); - const u2 = await usersCol.findOne({ _id: 'u2_rmval' }, { projection: { __rooms: 1 } }); - expect(u1?.__rooms || []).toContain(rid); - expect(u2?.__rooms || []).toContain(rid); + // nobody removed because removal only does not trigger reevaluation + const u1 = await usersCol.findOne({ _id: 'u1_rmval' }, { projection: { __rooms: 1 } }); + const u2 = await usersCol.findOne({ _id: 'u2_rmval' }, { projection: { __rooms: 1 } }); + expect(u1?.__rooms || []).toContain(rid); + expect(u2?.__rooms || []).toContain(rid); + }); }); }); describe('setRoomAbacAttributes - multi-attribute addition', () => { let rid: string; - beforeAll(async () => { + beforeEach(async () => { rid = await insertRoom([{ key: 'dept', values: ['eng'] }]); - await Promise.all([ - insertDefinitions([ - { key: 'dept', values: ['eng', 'sales', 'hr'] }, - { key: 'region', values: ['emea', 'apac'] }, - ]), - insertUsers([ - { - _id: 'u1_multi', - member: true, - extraRooms: [rid], - abacAttributes: [ - { key: 'dept', values: ['eng', 'sales'] }, - { key: 'region', values: ['emea'] }, - ], - }, // compliant after expansion - { - _id: 'u2_multi', - member: true, - extraRooms: [rid], - abacAttributes: [{ key: 'dept', values: ['eng'] }], // missing region - }, - { - _id: 'u3_multi', - member: true, - extraRooms: [rid], - abacAttributes: [{ key: 'region', values: ['emea'] }], // missing dept key - }, - { - _id: 'u4_multi', - member: true, - extraRooms: [rid], - abacAttributes: [ - { key: 'dept', values: ['eng', 'sales', 'hr'] }, - { key: 'region', values: ['emea', 'apac'] }, - ], - }, // superset across both - { - _id: 'u5_multi', - member: true, - extraRooms: [rid], - abacAttributes: [ - { key: 'dept', values: ['eng', 'sales'] }, - { key: 'region', values: ['apac'] }, - ], - }, - ]), + await insertDefinitions([ + { key: 'dept', values: ['eng', 'sales', 'hr'] }, + { key: 'region', values: ['emea', 'apac'] }, + ]); + + await insertUsers([ + { + _id: 'u1_multi', + member: true, + extraRooms: [rid], + abacAttributes: [ + { key: 'dept', values: ['eng', 'sales'] }, + { key: 'region', values: ['emea'] }, + ], + }, // compliant after expansion + { + _id: 'u2_multi', + member: true, + extraRooms: [rid], + abacAttributes: [{ key: 'dept', values: ['eng'] }], // missing region + }, + { + _id: 'u3_multi', + member: true, + extraRooms: [rid], + abacAttributes: [{ key: 'region', values: ['emea'] }], // missing dept key + }, + { + _id: 'u4_multi', + member: true, + extraRooms: [rid], + abacAttributes: [ + { key: 'dept', values: ['eng', 'sales', 'hr'] }, + { key: 'region', values: ['emea', 'apac'] }, + ], + }, // superset across both + { + _id: 'u5_multi', + member: true, + extraRooms: [rid], + abacAttributes: [ + { key: 'dept', values: ['eng', 'sales'] }, + { key: 'region', values: ['apac'] }, + ], + }, ]); - }, 30_000); + }); + + afterEach(async () => { + await usersCol.deleteMany({ + _id: { + $in: ['u1_multi', 'u2_multi', 'u3_multi', 'u4_multi', 'u5_multi'], + }, + }); + await roomsCol.deleteOne({ _id: rid }); + }); it('enforces all attributes (AND semantics) removing users failing any', async () => { await service.setRoomAbacAttributes( @@ -307,17 +338,23 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { describe('Idempotency & no-op behavior', () => { let rid: string; - beforeAll(async () => { + beforeEach(async () => { rid = await insertRoom([]); - await Promise.all([ - insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]), - insertUsers([ - { _id: 'u1_idem', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, - { _id: 'u2_idem', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // will be removed on first pass - ]), + await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); + await insertUsers([ + { _id: 'u1_idem', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, + { _id: 'u2_idem', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // will be removed on first pass ]); - }, 30_000); + }); + afterEach(async () => { + await usersCol.deleteMany({ + _id: { + $in: ['u1_idem', 'u2_idem'], + }, + }); + await roomsCol.deleteOne({ _id: rid }); + }); it('does not remove anyone when calling with identical attribute set twice', async () => { await service.setRoomAbacAttributes(rid, { dept: ['eng', 'sales'] }, fakeActor); @@ -349,33 +386,38 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { let ridSuperset: string; let ridMissingKey: string; - beforeAll(async () => { + beforeEach(async () => { ridSuperset = await insertRoom([]); - await Promise.all([ - insertDefinitions([{ key: 'dept', values: ['eng', 'sales', 'hr'] }]), - insertUsers([ - { - _id: 'u1_superset', - member: true, - extraRooms: [ridSuperset], - abacAttributes: [{ key: 'dept', values: ['eng', 'sales', 'hr'] }], - }, - { _id: 'u2_superset', member: true, extraRooms: [ridSuperset], abacAttributes: [{ key: 'dept', values: ['eng', 'hr'] }] }, // missing sales - ]), + await insertDefinitions([{ key: 'dept', values: ['eng', 'sales', 'hr'] }]); + await insertUsers([ + { + _id: 'u1_superset', + member: true, + extraRooms: [ridSuperset], + abacAttributes: [{ key: 'dept', values: ['eng', 'sales', 'hr'] }], + }, + { _id: 'u2_superset', member: true, extraRooms: [ridSuperset], abacAttributes: [{ key: 'dept', values: ['eng', 'hr'] }] }, // missing sales ]); ridMissingKey = await insertRoom([]); - await Promise.all([ - insertDefinitions([{ key: 'region', values: ['emea', 'apac'] }]), - insertUsers([ - { _id: 'u1_misskey', member: true, extraRooms: [ridMissingKey], abacAttributes: [{ key: 'region', values: ['emea'] }] }, - { _id: 'u2_misskey', member: true, extraRooms: [ridMissingKey], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing region - { _id: 'u3_misskey', member: true, extraRooms: [ridMissingKey] }, // no abacAttributes field - ]), + await insertDefinitions([{ key: 'region', values: ['emea', 'apac'] }]); + await insertUsers([ + { _id: 'u1_misskey', member: true, extraRooms: [ridMissingKey], abacAttributes: [{ key: 'region', values: ['emea'] }] }, + { _id: 'u2_misskey', member: true, extraRooms: [ridMissingKey], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing region + { _id: 'u3_misskey', member: true, extraRooms: [ridMissingKey] }, // no abacAttributes field ]); - }, 30_000); + }); + + afterEach(async () => { + await usersCol.deleteMany({ + _id: { + $in: ['u1_superset', 'u2_superset', 'u1_misskey', 'u2_misskey', 'u3_misskey'], + }, + }); + await roomsCol.deleteMany({ _id: { $in: [ridSuperset, ridMissingKey] } }); + }); it('keeps user with superset values and removes user missing one required value', async () => { await service.setRoomAbacAttributes(ridSuperset, { dept: ['eng', 'sales', 'hr'] }, fakeActor); @@ -406,11 +448,12 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { describe('Large member set performance sanity (lightweight)', () => { let rid: string; + let bulkIds: string[]; - beforeAll(async () => { + beforeEach(async () => { rid = await insertRoom([]); - await Promise.all([insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }])]); + await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); const bulk: Parameters[0] = []; for (let i = 0; i < 300; i++) { @@ -423,8 +466,16 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { abacAttributes: [{ key: 'dept', values }], }); } + bulkIds = bulk.map((u) => u._id); await insertUsers(bulk); - }, 30_000); + }); + + afterEach(async () => { + await usersCol.deleteMany({ + _id: { $in: bulkIds }, + }); + await roomsCol.deleteOne({ _id: rid }); + }); it('removes only expected fraction in a larger population', async () => { await service.setRoomAbacAttributes(rid, { dept: ['eng', 'sales'] }, fakeActor); From da094111711a571dba389c4ca09f708df58095a5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 7 Jan 2026 08:58:48 -0600 Subject: [PATCH 6/9] single mongo --- .../subject-attributes-validations.spec.ts | 25 ++--- .../src/test-helpers/mongoMemoryServer.ts | 94 +++++++++++++++++++ .../abac/src/user-auto-removal.spec.ts | 31 ++---- 3 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 ee/packages/abac/src/test-helpers/mongoMemoryServer.ts diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index 26640ee646551..d5c758b7036f7 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -1,10 +1,9 @@ import type { ILDAPEntry, IUser, IAbacAttributeDefinition } from '@rocket.chat/core-typings'; -import { registerModel, Users, UsersRaw, RoomsRaw, AbacAttributesRaw, ServerEventsRaw } from '@rocket.chat/models'; +import { Users } from '@rocket.chat/models'; import type { Collection, Db } from 'mongodb'; -import { MongoClient } from 'mongodb'; -import { MongoMemoryServer } from 'mongodb-memory-server'; import { AbacService } from './index'; +import { acquireSharedInMemoryMongo, SHARED_ABAC_TEST_DB, type SharedMongoConnection } from './test-helpers/mongoMemoryServer'; jest.mock('@rocket.chat/core-services', () => ({ ServiceClass: class {}, @@ -33,8 +32,7 @@ const makeLdap = (overrides: Partial = {}): ILDAPEntry => describe('Subject Attributes validation', () => { let db: Db; - let mongo: MongoMemoryServer; - let client: MongoClient; + let sharedMongo: SharedMongoConnection; const service = new AbacService(); let roomsCol: Collection; @@ -52,26 +50,15 @@ describe('Subject Attributes validation', () => { }; beforeAll(async () => { - mongo = await MongoMemoryServer.create(); - client = await MongoClient.connect(mongo.getUri(), {}); - db = client.db('abac_global'); - - // Register only the models we actually need for these tests - registerModel('IUsersModel', () => new UsersRaw(db)); - registerModel('IRoomsModel', () => new RoomsRaw(db)); - registerModel('IAbacAttributesModel', () => new AbacAttributesRaw(db)); - registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); - - // @ts-expect-error - ignore - await db.collection('abac_dummy_init').insertOne({ _id: 'init', createdAt: new Date(0) }); + sharedMongo = await acquireSharedInMemoryMongo(SHARED_ABAC_TEST_DB); + db = sharedMongo.db; roomsCol = db.collection('rocketchat_room'); usersCol = db.collection('users'); }, 30_000); afterAll(async () => { - await client.close(); - await mongo.stop(); + await sharedMongo.release(); }); beforeEach(async () => { diff --git a/ee/packages/abac/src/test-helpers/mongoMemoryServer.ts b/ee/packages/abac/src/test-helpers/mongoMemoryServer.ts new file mode 100644 index 0000000000000..71c22000be3fa --- /dev/null +++ b/ee/packages/abac/src/test-helpers/mongoMemoryServer.ts @@ -0,0 +1,94 @@ +import { registerModel, UsersRaw, RoomsRaw, AbacAttributesRaw, ServerEventsRaw, SubscriptionsRaw } from '@rocket.chat/models'; +import type { Db } from 'mongodb'; +import { MongoClient } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; + +export const SHARED_ABAC_TEST_DB = 'abac_test'; + +type SharedState = { + mongo: MongoMemoryServer; + client: MongoClient; + refCount: number; +}; + +let sharedState: SharedState | null = null; +let initialization: Promise | null = null; + +const ensureState = async (): Promise => { + if (sharedState) { + return sharedState; + } + + if (!initialization) { + initialization = (async () => { + const mongo = await MongoMemoryServer.create(); + const client = await MongoClient.connect(mongo.getUri(), {}); + return { + mongo, + client, + refCount: 0, + }; + })(); + } + + sharedState = await initialization; + initialization = null; + return sharedState; +}; + +const dropDatabase = async (db: Db) => { + try { + await db.dropDatabase(); + } catch (err) { + if (!(err instanceof Error) || !/ns not found/i.test(err.message)) { + throw err; + } + } +}; + +export type SharedMongoConnection = { + mongo: MongoMemoryServer; + client: MongoClient; + db: Db; + cleanupDatabase: () => Promise; + release: () => Promise; +}; + +const registerAbacTestModels = (db: Db) => { + registerModel('IUsersModel', () => new UsersRaw(db)); + registerModel('IRoomsModel', () => new RoomsRaw(db)); + registerModel('IAbacAttributesModel', () => new AbacAttributesRaw(db)); + registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); + registerModel('ISubscriptionsModel', () => new SubscriptionsRaw(db)); +}; + +export const acquireSharedInMemoryMongo = async (dbName: string): Promise => { + const state = await ensureState(); + state.refCount += 1; + + const connectionDb = state.client.db(dbName); + let released = false; + + registerAbacTestModels(connectionDb); + return { + mongo: state.mongo, + client: state.client, + db: connectionDb, + cleanupDatabase: async () => dropDatabase(connectionDb), + release: async () => { + if (released || !sharedState) { + return; + } + + released = true; + sharedState.refCount -= 1; + + if (sharedState.refCount === 0) { + const { client, mongo } = sharedState; + sharedState = null; + await client.close(); + await mongo.stop(); + } + }, + }; +}; diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index a0931fcd44113..8b865eb094615 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -1,19 +1,10 @@ import type { IAbacAttributeDefinition, IRoom, IUser } from '@rocket.chat/core-typings'; -import { - registerModel, - Subscriptions, - SubscriptionsRaw, - UsersRaw, - RoomsRaw, - AbacAttributesRaw, - ServerEventsRaw, -} from '@rocket.chat/models'; +import { Subscriptions } from '@rocket.chat/models'; import type { Collection, Db } from 'mongodb'; -import { MongoClient } from 'mongodb'; -import { MongoMemoryServer } from 'mongodb-memory-server'; import { Audit } from './audit'; import { AbacService } from './index'; +import { acquireSharedInMemoryMongo, SHARED_ABAC_TEST_DB, type SharedMongoConnection } from './test-helpers/mongoMemoryServer'; jest.mock('@rocket.chat/core-services', () => ({ ServiceClass: class {}, @@ -26,8 +17,7 @@ jest.mock('@rocket.chat/core-services', () => ({ })); describe('AbacService integration (onRoomAttributesChanged)', () => { - let mongo: MongoMemoryServer; - let client: MongoClient; + let sharedMongo: SharedMongoConnection; let db: Db; const service = new AbacService(); @@ -87,16 +77,8 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { let auditSpy: jest.SpyInstance; beforeAll(async () => { - mongo = await MongoMemoryServer.create(); - client = await MongoClient.connect(mongo.getUri(), {}); - db = client.db('abac_integration'); - - // Register only the models we actually need for these tests - registerModel('IUsersModel', () => new UsersRaw(db)); - registerModel('IRoomsModel', () => new RoomsRaw(db)); - registerModel('IAbacAttributesModel', () => new AbacAttributesRaw(db)); - registerModel('IServerEventsModel', () => new ServerEventsRaw(db)); - registerModel('ISubscriptionsModel', () => new SubscriptionsRaw(db)); + sharedMongo = await acquireSharedInMemoryMongo(SHARED_ABAC_TEST_DB); + db = sharedMongo.db; debugSpy = jest.spyOn((service as any).logger, 'debug').mockImplementation(() => undefined); auditSpy = jest.spyOn(Audit, 'actionPerformed').mockResolvedValue(); @@ -106,8 +88,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }, 30_000); afterAll(async () => { - await client.close(); - await mongo.stop(); + await sharedMongo.release(); }); beforeEach(async () => { From 8103e0962965a699d57fe89fbc44ff50c825e39f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 7 Jan 2026 09:48:40 -0600 Subject: [PATCH 7/9] new attempt --- .../subject-attributes-validations.spec.ts | 396 ++++++++++-------- .../abac/src/user-auto-removal.spec.ts | 171 +++++--- 2 files changed, 347 insertions(+), 220 deletions(-) diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index d5c758b7036f7..0d43e26d41bee 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -30,62 +30,169 @@ const makeLdap = (overrides: Partial = {}): ILDAPEntry => ...overrides, }) as ILDAPEntry; -describe('Subject Attributes validation', () => { - let db: Db; - let sharedMongo: SharedMongoConnection; - const service = new AbacService(); - - let roomsCol: Collection; - let usersCol: Collection; - - const insertRooms = async (rooms: { _id: string; abacAttributes?: IAbacAttributeDefinition[] }[]) => { - await roomsCol.insertMany( - rooms.map((room) => ({ - _id: room._id, - name: room._id, - t: 'p', - abacAttributes: room.abacAttributes, - })), - ); +type StaticUserDefinition = { + _id: string; + username: string; +}; + +const staticUserDefinitions: StaticUserDefinition[] = [ + { _id: 'u-no-map-attrs', username: 'user-no-map' }, + { _id: 'u-merge-memberof', username: 'merge-memberof' }, + { _id: 'u-distinct-keys', username: 'distinct-keys' }, + { _id: 'u-merge-array-string', username: 'merge-array-string' }, + { _id: 'u-unset-loss', username: 'unset-loss' }, + { _id: 'u-unset-none', username: 'unset-none' }, + { _id: 'u-loss-values', username: 'loss-values' }, + { _id: 'u-loss-key', username: 'loss-key' }, + { _id: 'u-gain-values', username: 'gain-values' }, + { _id: 'u-order-only', username: 'order-only' }, + { _id: 'u-merge-ldap-dup', username: 'merge-ldap-dup' }, + { _id: 'u-immutability', username: 'immutability' }, + { _id: 'u-loss', username: 'lossy' }, + { _id: 'u-key-loss', username: 'keyloss' }, + { _id: 'u-growth', username: 'growth' }, + { _id: 'u-extra-room-key', username: 'extrakey' }, + { _id: 'u-empty', username: 'empty' }, + { _id: 'u-lose-unrelated', username: 'unrelated' }, +]; + +const staticTestUsers: IUser[] = staticUserDefinitions.map(({ _id, username }) => ({ + _id, + username, + roles: [], + type: 'user', + active: true, + createdAt: new Date(0), + _updatedAt: new Date(0), + __rooms: [], +})) as IUser[]; + +const staticUserIds = staticTestUsers.map(({ _id }) => _id); +const staticUserBaseMap = Object.fromEntries(staticTestUsers.map((user) => [user._id, { ...user }])) as Record; + +const getStaticUser = (_id: string, overrides: Partial = {}): IUser => { + const base = staticUserBaseMap[_id]; + if (!base) { + throw new Error(`Unknown static user ${_id}`); + } + return { + ...base, + ...overrides, + _id: base._id, + username: overrides.username ?? base.username, + __rooms: overrides.__rooms ?? base.__rooms ?? [], }; +}; + +type StaticUserUpdate = Partial & { _id: string }; + +const service = new AbacService(); + +let db: Db; +let sharedMongo: SharedMongoConnection; +let roomsCol: Collection; +let usersCol: Collection; +const resetStaticUsers = async () => { + await usersCol.updateMany( + { _id: { $in: staticUserIds } }, + { + $set: { + __rooms: [], + _updatedAt: new Date(), + }, + $unset: { + abacAttributes: 1, + }, + }, + ); +}; + +const configureStaticUsers = async (users: StaticUserUpdate[]) => { + if (!usersCol) { + throw new Error('users collection not initialized'); + } + await Promise.all( + users.map(async ({ _id, ...fields }) => { + const update: { $set: Record; $unset?: Record } = { + $set: { _updatedAt: new Date() }, + }; + + for (const [key, value] of Object.entries(fields)) { + if (key === 'abacAttributes') { + if (value === undefined) { + update.$unset = { ...(update.$unset || {}), abacAttributes: 1 }; + } else { + update.$set.abacAttributes = value; + } + continue; + } + if (key === '__rooms') { + update.$set.__rooms = value ?? []; + continue; + } + update.$set[key] = value; + } + + const result = await usersCol.updateOne({ _id }, update); + if (!result.matchedCount) { + throw new Error(`Static test user ${_id} not initialized`); + } + }), + ); +}; + +const makeLdapEntry = makeLdap; // preserve existing helper naming intent + +const insertRooms = async (rooms: { _id: string; abacAttributes?: IAbacAttributeDefinition[] }[]) => { + await roomsCol.insertMany( + rooms.map((room) => ({ + _id: room._id, + name: room._id, + t: 'p', + abacAttributes: room.abacAttributes, + })), + ); +}; + +describe('Subject Attributes validation', () => { beforeAll(async () => { sharedMongo = await acquireSharedInMemoryMongo(SHARED_ABAC_TEST_DB); db = sharedMongo.db; roomsCol = db.collection('rocketchat_room'); usersCol = db.collection('users'); + + await usersCol.deleteMany({ _id: { $in: staticUserIds } }); + await usersCol.insertMany(staticTestUsers); }, 30_000); afterAll(async () => { + await usersCol.deleteMany({ _id: { $in: staticUserIds } }); await sharedMongo.release(); }); beforeEach(async () => { - // Clear state between tests while reusing the same in-memory DB & models - await Promise.all([usersCol.deleteMany({}), roomsCol.deleteMany({})]); + await resetStaticUsers(); + await roomsCol.deleteMany({}); }); - const insertUser = async (user: IUser) => { - await usersCol.insertOne(user); - }; - describe('AbacService.addSubjectAttributes (unit)', () => { describe('early returns and no-ops', () => { it('returns early when user has no _id', async () => { const user = makeUser({ _id: undefined }); await service.addSubjectAttributes(user, makeLdap(), { group: 'dept' }); - // Nothing inserted, ensure no user doc created const found = await Users.findOne({ username: user.username }); expect(found).toBeFalsy(); }); it('does nothing (no update) when map produces no attributes and user had none', async () => { - const user = makeUser({ abacAttributes: undefined }); - await insertUser(user); - const ldap = makeLdap({ group: '' }); + const userId = 'u-no-map-attrs'; + await configureStaticUsers([{ _id: userId, abacAttributes: undefined }]); + const user = getStaticUser(userId); + const ldap = makeLdapEntry({ group: '' }); await service.addSubjectAttributes(user, ldap, { group: 'dept' }); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated).toBeTruthy(); expect(updated?.abacAttributes ?? undefined).toBeUndefined(); }); @@ -93,29 +200,31 @@ describe('Subject Attributes validation', () => { describe('building and setting attributes', () => { it('merges multiple LDAP keys mapping to the same ABAC key, deduplicating values', async () => { - const user = makeUser(); - await insertUser(user); - const ldap = makeLdap({ + const userId = 'u-merge-memberof'; + await configureStaticUsers([{ _id: userId }]); + const user = getStaticUser(userId); + const ldap = makeLdapEntry({ memberOf: ['eng', 'sales', 'eng'], department: ['sales', 'support'], }); const map = { memberOf: 'dept', department: 'dept' }; await service.addSubjectAttributes(user, ldap, map); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([{ key: 'dept', values: ['eng', 'sales', 'support'] }]); }); it('creates distinct ABAC attributes for different mapped keys preserving insertion order', async () => { - const user = makeUser(); - await insertUser(user); - const ldap = makeLdap({ + const userId = 'u-distinct-keys'; + await configureStaticUsers([{ _id: userId }]); + const user = getStaticUser(userId); + const ldap = makeLdapEntry({ groups: ['alpha', 'beta'], regionCodes: ['emea', 'apac'], role: 'admin', }); const map = { groups: 'team', regionCodes: 'region', role: 'role' }; await service.addSubjectAttributes(user, ldap, map); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([ { key: 'team', values: ['alpha', 'beta'] }, { key: 'region', values: ['emea', 'apac'] }, @@ -124,87 +233,103 @@ describe('Subject Attributes validation', () => { }); it('merges array and string LDAP values into one attribute', async () => { - const user = makeUser(); - await insertUser(user); - const ldap = makeLdap({ deptCode: 'eng', deptName: ['engineering', 'eng'] }); + const userId = 'u-merge-array-string'; + await configureStaticUsers([{ _id: userId }]); + const user = getStaticUser(userId); + const ldap = makeLdapEntry({ deptCode: 'eng', deptName: ['engineering', 'eng'] }); const map = { deptCode: 'dept', deptName: 'dept' }; await service.addSubjectAttributes(user, ldap, map); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([{ key: 'dept', values: ['eng', 'engineering'] }]); }); }); describe('unsetting attributes when none extracted', () => { it('unsets abacAttributes when user previously had attributes but now extracts none', async () => { - const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }); - await insertUser(user); - const ldap = makeLdap({ other: ['x'] }); + const userId = 'u-unset-loss'; + await configureStaticUsers([{ _id: userId, abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }]); + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }); + const ldap = makeLdapEntry({ other: ['x'] }); const map = { missing: 'dept' }; await service.addSubjectAttributes(user, ldap, map); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toBeUndefined(); }); it('does not unset when user had no prior attributes and extraction yields none', async () => { - const user = makeUser({ abacAttributes: [] }); - await insertUser(user); - const ldap = makeLdap({}); + const userId = 'u-unset-none'; + await configureStaticUsers([{ _id: userId, abacAttributes: [] }]); + const user = getStaticUser(userId, { abacAttributes: [] }); + const ldap = makeLdapEntry({}); const map = { missing: 'dept' }; await service.addSubjectAttributes(user, ldap, map); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([]); }); }); describe('loss detection triggering hook (attribute changes)', () => { it('updates attributes reducing values on loss', async () => { - const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }); - await insertUser(user); - const ldap = makeLdap({ memberOf: ['eng'] }); + const userId = 'u-loss-values'; + await configureStaticUsers([{ _id: userId, abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }]); + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }); + const ldap = makeLdapEntry({ memberOf: ['eng'] }); await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([{ key: 'dept', values: ['eng'] }]); }); it('updates attributes removing an entire key', async () => { - const user = makeUser({ + const userId = 'u-loss-key'; + await configureStaticUsers([ + { + _id: userId, + abacAttributes: [ + { key: 'dept', values: ['eng'] }, + { key: 'region', values: ['emea'] }, + ], + }, + ]); + const user = getStaticUser(userId, { abacAttributes: [ { key: 'dept', values: ['eng'] }, { key: 'region', values: ['emea'] }, ], }); - await insertUser(user); - const ldap = makeLdap({ department: ['eng'] }); + const ldap = makeLdapEntry({ department: ['eng'] }); await service.addSubjectAttributes(user, ldap, { department: 'dept' }); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([{ key: 'dept', values: ['eng'] }]); }); it('gains new values without triggering loss logic', async () => { - const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng'] }] }); - await insertUser(user); - const ldap = makeLdap({ memberOf: ['eng', 'qa'] }); + const userId = 'u-gain-values'; + await configureStaticUsers([{ _id: userId, abacAttributes: [{ key: 'dept', values: ['eng'] }] }]); + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng'] }] }); + const ldap = makeLdapEntry({ memberOf: ['eng', 'qa'] }); await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([{ key: 'dept', values: ['eng', 'qa'] }]); }); it('keeps attributes unchanged when only ordering differs', async () => { - const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }); - await insertUser(user); - const ldap = makeLdap({ memberOf: ['qa', 'eng'] }); + const userId = 'u-order-only'; + await configureStaticUsers([{ _id: userId, abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }]); + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }); + const ldap = makeLdapEntry({ memberOf: ['qa', 'eng'] }); await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes?.[0].key).toBe('dept'); expect(new Set(updated?.abacAttributes?.[0].values)).toEqual(new Set(['eng', 'qa'])); }); it('merges duplicate LDAP mapping keys retaining union of values', async () => { - const user = makeUser({ abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }); - await insertUser(user); - const ldap = makeLdap({ deptA: ['eng', 'sales'], deptB: ['eng'] }); + const userId = 'u-merge-ldap-dup'; + await configureStaticUsers([{ _id: userId, abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }]); + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }); + const ldap = makeLdapEntry({ deptA: ['eng', 'sales'], deptB: ['eng'] }); await service.addSubjectAttributes(user, ldap, { deptA: 'dept', deptB: 'dept' }); - const updated = await Users.findOneById(user._id, { projection: { abacAttributes: 1 } }); + const updated = await Users.findOneById(userId, { projection: { abacAttributes: 1 } }); expect(updated?.abacAttributes).toEqual([{ key: 'dept', values: ['eng', 'sales'] }]); }); }); @@ -214,19 +339,18 @@ describe('Subject Attributes validation', () => { let original: IAbacAttributeDefinition[]; let clone: IAbacAttributeDefinition[]; - beforeAll(async () => { + beforeAll(() => { original = [{ key: 'dept', values: ['eng', 'sales'] }] as IAbacAttributeDefinition[]; clone = JSON.parse(JSON.stringify(original)); - sharedUser = makeUser({ abacAttributes: original }); - await insertUser(sharedUser); }); - afterAll(async () => { - await usersCol.deleteOne({ _id: sharedUser._id }); + beforeEach(async () => { + await configureStaticUsers([{ _id: 'u-immutability', abacAttributes: original }]); + sharedUser = getStaticUser('u-immutability', { abacAttributes: original }); }); it('does not mutate original user.abacAttributes array reference contents', async () => { - const ldap = makeLdap({ memberOf: ['eng', 'sales', 'support'] }); + const ldap = makeLdapEntry({ memberOf: ['eng', 'sales', 'support'] }); await service.addSubjectAttributes(sharedUser, ldap, { memberOf: 'dept' }); expect(original).toEqual(clone); }); @@ -236,39 +360,22 @@ describe('Subject Attributes validation', () => { describe('AbacService.addSubjectAttributes (room removals)', () => { const originalCoreServices = jest.requireMock('@rocket.chat/core-services'); originalCoreServices.Room.removeUserFromRoom = async (rid: string, user: IUser) => { - // @ts-expect-error - test await usersCol.updateOne({ _id: user._id }, { $pull: { __rooms: rid } }); }; - const insertUserForRemovalTests = async (user: IUser & { __rooms?: string[] }) => - usersCol.insertOne({ - ...user, - __rooms: user.__rooms || [], - }); - it('removes user from rooms whose attributes become non-compliant after losing a value', async () => { - const user: IUser = { - _id: 'u-loss', - username: 'lossy', - roles: [], - type: 'user', - active: true, - createdAt: new Date(), - _updatedAt: new Date(), + const userId = 'u-loss'; + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }], __rooms: ['rKeep', 'rRemove'], - }; + }); + await configureStaticUsers([{ _id: userId, abacAttributes: user.abacAttributes, __rooms: user.__rooms }]); - // Rooms: - // rKeep requires only 'eng' (will remain compliant) - // rRemove requires both 'eng' and 'qa' (will become non-compliant after loss) await insertRooms([ { _id: 'rKeep', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, { _id: 'rRemove', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, ]); - await insertUserForRemovalTests({ ...user, __rooms: ['rKeep', 'rRemove'] }); - const ldap: ILDAPEntry = { memberOf: ['eng'], _raw: {}, @@ -276,32 +383,23 @@ describe('Subject Attributes validation', () => { await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); - const updatedUser = await usersCol.findOne({ _id: user._id }, { projection: { abacAttributes: 1, __rooms: 1 } }); + const updatedUser = await usersCol.findOne({ _id: userId }, { projection: { abacAttributes: 1, __rooms: 1 } }); expect(updatedUser?.abacAttributes).toEqual([{ key: 'dept', values: ['eng'] }]); expect(updatedUser?.abacAttributes).not.toEqual(user.abacAttributes); - expect(updatedUser?.__rooms.sort()).toEqual(['rKeep']); + expect(updatedUser?.__rooms?.sort()).toEqual(['rKeep']); }); it('removes user from rooms containing attribute keys they no longer possess (key loss)', async () => { - const user: IUser = { - _id: 'u-key-loss', - username: 'keyloss', - roles: [], - type: 'user', - active: true, - createdAt: new Date(), - _updatedAt: new Date(), + const userId = 'u-key-loss'; + const user = getStaticUser(userId, { abacAttributes: [ { key: 'dept', values: ['eng'] }, { key: 'region', values: ['emea'] }, ], __rooms: ['rDeptOnly', 'rRegionOnly', 'rBoth'], - }; + }); + await configureStaticUsers([{ _id: userId, abacAttributes: user.abacAttributes, __rooms: user.__rooms }]); - // Rooms: - // rDeptOnly -> only dept (will stay) - // rRegionOnly -> region only (will be removed after region key loss) - // rBoth -> both dept & region (will be removed) await insertRooms([ { _id: 'rDeptOnly', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, { _id: 'rRegionOnly', abacAttributes: [{ key: 'region', values: ['emea'] }] }, @@ -314,8 +412,6 @@ describe('Subject Attributes validation', () => { }, ]); - await insertUserForRemovalTests({ ...user, __rooms: ['rDeptOnly', 'rRegionOnly', 'rBoth'] }); - const ldap: ILDAPEntry = { department: ['eng'], _raw: {}, @@ -323,32 +419,25 @@ describe('Subject Attributes validation', () => { await service.addSubjectAttributes(user, ldap, { department: 'dept' }); - const updatedUser = await usersCol.findOne({ _id: user._id }, { projection: { abacAttributes: 1, __rooms: 1 } }); + const updatedUser = await usersCol.findOne({ _id: userId }, { projection: { abacAttributes: 1, __rooms: 1 } }); expect(updatedUser?.abacAttributes).toEqual([{ key: 'dept', values: ['eng'] }]); expect(updatedUser?.abacAttributes).not.toEqual(user.abacAttributes); expect(updatedUser?.__rooms).toEqual(['rDeptOnly']); }); it('does not remove user from any room when attribute values only grow (gain without loss)', async () => { - const user: IUser = { - _id: 'u-growth', - username: 'growth', - roles: [], - type: 'user', - active: true, - createdAt: new Date(), - _updatedAt: new Date(), + const userId = 'u-growth'; + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng'] }], __rooms: ['rGrowthA', 'rGrowthB'], - }; + }); + await configureStaticUsers([{ _id: userId, abacAttributes: user.abacAttributes, __rooms: user.__rooms }]); await insertRooms([ { _id: 'rGrowthA', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, - { _id: 'rGrowthB', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, // superset; still compliant after growth + { _id: 'rGrowthB', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, ]); - await insertUserForRemovalTests({ ...user, __rooms: ['rGrowthA', 'rGrowthB'] }); - const ldap: ILDAPEntry = { memberOf: ['eng', 'qa'], _raw: {}, @@ -356,26 +445,21 @@ describe('Subject Attributes validation', () => { await service.addSubjectAttributes(user, ldap, { memberOf: 'dept' }); - const updatedUser = await usersCol.findOne({ _id: user._id }, { projection: { abacAttributes: 1, __rooms: 1 } }); + const updatedUser = await usersCol.findOne({ _id: userId }, { projection: { abacAttributes: 1, __rooms: 1 } }); expect(updatedUser?.abacAttributes).toEqual([{ key: 'dept', values: ['eng', 'qa'] }]); - expect(updatedUser?.__rooms.sort()).toEqual(['rGrowthA', 'rGrowthB']); + expect(updatedUser?.__rooms?.sort()).toEqual(['rGrowthA', 'rGrowthB']); }); it('removes user from rooms having attribute keys not present in new attribute set (extra keys in room)', async () => { - const user: IUser = { - _id: 'u-extra-room-key', - username: 'extrakey', - roles: [], - type: 'user', - active: true, - createdAt: new Date(), - _updatedAt: new Date(), + const userId = 'u-extra-room-key'; + const user = getStaticUser(userId, { abacAttributes: [ { key: 'dept', values: ['eng', 'sales'] }, { key: 'otherKey', values: ['value'] }, ], __rooms: ['rExtraKeyRoom', 'rBaseline'], - }; + }); + await configureStaticUsers([{ _id: userId, abacAttributes: user.abacAttributes, __rooms: user.__rooms }]); await insertRooms([ { @@ -391,8 +475,6 @@ describe('Subject Attributes validation', () => { }, ]); - await insertUserForRemovalTests({ ...user, __rooms: ['rExtraKeyRoom', 'rBaseline'] }); - const ldap: ILDAPEntry = { deptCodes: ['eng', 'sales'], _raw: {}, @@ -400,31 +482,24 @@ describe('Subject Attributes validation', () => { await service.addSubjectAttributes(user, ldap, { deptCodes: 'dept' }); - const updatedUser = await usersCol.findOne({ _id: user._id }, { projection: { abacAttributes: 1, __rooms: 1 } }); + const updatedUser = await usersCol.findOne({ _id: userId }, { projection: { abacAttributes: 1, __rooms: 1 } }); expect(updatedUser?.abacAttributes).toEqual([{ key: 'dept', values: ['eng', 'sales'] }]); - expect(updatedUser?.__rooms.sort()).toEqual(['rBaseline']); + expect(updatedUser?.__rooms?.sort()).toEqual(['rBaseline']); }); it('unsets attributes and removes user from all ABAC rooms when no LDAP values extracted', async () => { - const user: IUser = { - _id: 'u-empty', - username: 'empty', - roles: [], - type: 'user', - active: true, - createdAt: new Date(), - _updatedAt: new Date(), + const userId = 'u-empty'; + const user = getStaticUser(userId, { abacAttributes: [{ key: 'dept', values: ['eng'] }], __rooms: ['rAny1', 'rAny2'], - }; + }); + await configureStaticUsers([{ _id: userId, abacAttributes: user.abacAttributes, __rooms: user.__rooms }]); await insertRooms([ { _id: 'rAny1', abacAttributes: [{ key: 'dept', values: ['eng'] }] }, { _id: 'rAny2', abacAttributes: [{ key: 'dept', values: ['eng', 'qa'] }] }, ]); - await insertUserForRemovalTests({ ...user, __rooms: ['rAny1', 'rAny2'] }); - const ldap: ILDAPEntry = { unrelated: ['x'], _raw: {}, @@ -432,28 +507,23 @@ describe('Subject Attributes validation', () => { await service.addSubjectAttributes(user, ldap, { missing: 'dept' }); - const updatedUser = await usersCol.findOne({ _id: user._id }, { projection: { abacAttributes: 1, __rooms: 1 } }); + const updatedUser = await usersCol.findOne({ _id: userId }, { projection: { abacAttributes: 1, __rooms: 1 } }); expect(updatedUser?.abacAttributes).toBeUndefined(); expect(updatedUser?.abacAttributes).not.toEqual(user.abacAttributes); expect(updatedUser?.__rooms).toEqual([]); }); it('does not remove user from room when losing attribute not used by room (hook runs but no change)', async () => { - const user: IUser = { - _id: 'u-lose-unrelated', - username: 'unrelated', - roles: [], - type: 'user', - active: true, - createdAt: new Date(), - _updatedAt: new Date(), + const userId = 'u-lose-unrelated'; + const user = getStaticUser(userId, { abacAttributes: [ { key: 'dept', values: ['eng'] }, { key: 'region', values: ['emea'] }, { key: 'project', values: ['X'] }, ], __rooms: ['rDeptRegion'], - }; + }); + await configureStaticUsers([{ _id: userId, abacAttributes: user.abacAttributes, __rooms: user.__rooms }]); await insertRooms([ { @@ -465,8 +535,6 @@ describe('Subject Attributes validation', () => { }, ]); - await insertUserForRemovalTests({ ...user, __rooms: ['rDeptRegion'] }); - const ldap: ILDAPEntry = { department: ['eng', 'ceo'], regionCodes: ['emea', 'apac'], @@ -475,7 +543,7 @@ describe('Subject Attributes validation', () => { await service.addSubjectAttributes(user, ldap, { department: 'dept', regionCodes: 'region', projectCodes: 'project' }); - const updatedUser = await usersCol.findOne({ _id: user._id }, { projection: { abacAttributes: 1, __rooms: 1 } }); + const updatedUser = await usersCol.findOne({ _id: userId }, { projection: { abacAttributes: 1, __rooms: 1 } }); expect(updatedUser?.abacAttributes).toEqual([ { key: 'dept', values: ['eng', 'ceo'] }, { key: 'region', values: ['emea', 'apac'] }, diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index 8b865eb094615..cc78b75c1560e 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -50,26 +50,111 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { return rid; }; - const insertUsers = async ( - users: Array<{ - _id: string; - abacAttributes?: IAbacAttributeDefinition[]; - member?: boolean; - extraRooms?: string[]; - }>, - ) => { + type TestUserSeed = { + _id: string; + abacAttributes?: IAbacAttributeDefinition[]; + member?: boolean; + extraRooms?: string[]; + }; + + const insertUsers = async (users: TestUserSeed[]) => { await usersCol.insertMany( - users.map((u) => ({ - _id: u._id, - username: u._id, - type: 'user', - roles: [], - active: true, - createdAt: new Date(), - _updatedAt: new Date(), - abacAttributes: u.abacAttributes, - __rooms: u.extraRooms || [], - })), + users.map((u) => { + const doc: Partial & { + _id: string; + username: string; + type: IUser['type']; + roles: IUser['roles']; + active: boolean; + createdAt: Date; + _updatedAt: Date; + __rooms: string[]; + } = { + _id: u._id, + username: u._id, + type: 'user', + roles: [], + active: true, + createdAt: new Date(), + _updatedAt: new Date(), + __rooms: u.extraRooms || [], + }; + if (u.abacAttributes !== undefined) { + doc.abacAttributes = u.abacAttributes; + } + return doc as IUser; + }), + ); + }; + + const staticUserIds = [ + 'u1_newkey', + 'u2_newkey', + 'u3_newkey', + 'u4_newkey', + 'u5_newkey', + 'u1_dupvals', + 'u2_dupvals', + 'u1_newval', + 'u2_newval', + 'u3_newval', + 'u1_rmval', + 'u2_rmval', + 'u1_multi', + 'u2_multi', + 'u3_multi', + 'u4_multi', + 'u5_multi', + 'u1_idem', + 'u2_idem', + 'u1_superset', + 'u2_superset', + 'u1_misskey', + 'u2_misskey', + 'u3_misskey', + ]; + + const staticTestUsers: TestUserSeed[] = staticUserIds.map((_id) => ({ _id })); + + const resetStaticUsers = async () => { + await usersCol.updateMany( + { _id: { $in: staticUserIds } }, + { + $set: { + __rooms: [], + _updatedAt: new Date(), + }, + $unset: { + abacAttributes: 1, + }, + }, + ); + }; + + const configureStaticUsers = async (users: TestUserSeed[]) => { + await Promise.all( + users.map(async (user) => { + const setPayload: Partial = { + __rooms: user.extraRooms ?? [], + _updatedAt: new Date(), + }; + if (user.abacAttributes !== undefined) { + setPayload.abacAttributes = user.abacAttributes; + } + const update: { + $set: Partial; + $unset?: Record; + } = { + $set: setPayload, + }; + if (user.abacAttributes === undefined) { + update.$unset = { abacAttributes: 1 }; + } + const result = await usersCol.updateOne({ _id: user._id }, update); + if (result.matchedCount === 0) { + throw new Error(`Static test user ${user._id} not initialized`); + } + }), ); }; @@ -85,15 +170,19 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { roomsCol = db.collection('rocketchat_room'); usersCol = db.collection('users'); + await usersCol.deleteMany({ _id: { $in: staticUserIds } }); + await insertUsers(staticTestUsers); }, 30_000); afterAll(async () => { + await usersCol.deleteMany({ _id: { $in: staticUserIds } }); await sharedMongo.release(); }); beforeEach(async () => { debugSpy.mockClear(); auditSpy.mockClear(); + await resetStaticUsers(); }); describe('setRoomAbacAttributes - new key addition', () => { @@ -109,7 +198,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { { key: 'dep2t', values: ['eng', 'sales'] }, ]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // compliant { _id: 'u2_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing sales { _id: 'u3_newkey', member: true, extraRooms: [rid1], abacAttributes: [{ key: 'location', values: ['emea'] }] }, // missing dept key @@ -121,11 +210,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }); afterEach(async () => { - await usersCol.deleteMany({ - _id: { - $in: ['u1_newkey', 'u2_newkey', 'u3_newkey', 'u4_newkey', 'u5_newkey', 'u1_dupvals', 'u2_dupvals'], - }, - }); await roomsCol.deleteMany({ _id: { $in: [rid1, rid2] } }); }); @@ -171,7 +255,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { rid = await insertRoom([{ key: 'dept', values: ['eng'] }]); await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_newval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, // already superset { _id: 'u2_newval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing new value { _id: 'u3_newval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales', 'hr'] }] }, // superset @@ -179,11 +263,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }); afterEach(async () => { - await usersCol.deleteMany({ - _id: { - $in: ['u1_newval', 'u2_newval', 'u3_newval'], - }, - }); await roomsCol.deleteOne({ _id: rid }); }); @@ -204,18 +283,13 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { rid = await insertRoom([{ key: 'dept', values: ['eng', 'sales'] }]); await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_rmval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, { _id: 'u2_rmval', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, ]); }); afterEach(async () => { - await usersCol.deleteMany({ - _id: { - $in: ['u1_rmval', 'u2_rmval'], - }, - }); await roomsCol.deleteOne({ _id: rid }); }); @@ -244,7 +318,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { { key: 'region', values: ['emea', 'apac'] }, ]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_multi', member: true, @@ -288,11 +362,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }); afterEach(async () => { - await usersCol.deleteMany({ - _id: { - $in: ['u1_multi', 'u2_multi', 'u3_multi', 'u4_multi', 'u5_multi'], - }, - }); await roomsCol.deleteOne({ _id: rid }); }); @@ -323,17 +392,12 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { rid = await insertRoom([]); await insertDefinitions([{ key: 'dept', values: ['eng', 'sales'] }]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_idem', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng', 'sales'] }] }, { _id: 'u2_idem', member: true, extraRooms: [rid], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // will be removed on first pass ]); }); afterEach(async () => { - await usersCol.deleteMany({ - _id: { - $in: ['u1_idem', 'u2_idem'], - }, - }); await roomsCol.deleteOne({ _id: rid }); }); @@ -371,7 +435,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { ridSuperset = await insertRoom([]); await insertDefinitions([{ key: 'dept', values: ['eng', 'sales', 'hr'] }]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_superset', member: true, @@ -384,7 +448,7 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { ridMissingKey = await insertRoom([]); await insertDefinitions([{ key: 'region', values: ['emea', 'apac'] }]); - await insertUsers([ + await configureStaticUsers([ { _id: 'u1_misskey', member: true, extraRooms: [ridMissingKey], abacAttributes: [{ key: 'region', values: ['emea'] }] }, { _id: 'u2_misskey', member: true, extraRooms: [ridMissingKey], abacAttributes: [{ key: 'dept', values: ['eng'] }] }, // missing region { _id: 'u3_misskey', member: true, extraRooms: [ridMissingKey] }, // no abacAttributes field @@ -392,11 +456,6 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }); afterEach(async () => { - await usersCol.deleteMany({ - _id: { - $in: ['u1_superset', 'u2_superset', 'u1_misskey', 'u2_misskey', 'u3_misskey'], - }, - }); await roomsCol.deleteMany({ _id: { $in: [ridSuperset, ridMissingKey] } }); }); From 4e483680e8311ab52d7bb16372fe2eb3df2f167c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 7 Jan 2026 09:50:31 -0600 Subject: [PATCH 8/9] another --- .../subject-attributes-validations.spec.ts | 51 ++++++++-------- .../abac/src/user-auto-removal.spec.ts | 58 +++++++++++-------- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/ee/packages/abac/src/subject-attributes-validations.spec.ts b/ee/packages/abac/src/subject-attributes-validations.spec.ts index 0d43e26d41bee..8ededa04cee40 100644 --- a/ee/packages/abac/src/subject-attributes-validations.spec.ts +++ b/ee/packages/abac/src/subject-attributes-validations.spec.ts @@ -112,34 +112,37 @@ const configureStaticUsers = async (users: StaticUserUpdate[]) => { if (!usersCol) { throw new Error('users collection not initialized'); } - await Promise.all( - users.map(async ({ _id, ...fields }) => { - const update: { $set: Record; $unset?: Record } = { - $set: { _updatedAt: new Date() }, - }; - for (const [key, value] of Object.entries(fields)) { - if (key === 'abacAttributes') { - if (value === undefined) { - update.$unset = { ...(update.$unset || {}), abacAttributes: 1 }; - } else { - update.$set.abacAttributes = value; - } - continue; - } - if (key === '__rooms') { - update.$set.__rooms = value ?? []; - continue; + const operations = users.map(({ _id, ...fields }) => { + const update: { $set: Record; $unset?: Record } = { + $set: { _updatedAt: new Date() }, + }; + + for (const [key, value] of Object.entries(fields)) { + if (key === 'abacAttributes') { + if (value === undefined) { + update.$unset = { ...(update.$unset || {}), abacAttributes: 1 }; + } else { + update.$set.abacAttributes = value; } - update.$set[key] = value; + continue; } - - const result = await usersCol.updateOne({ _id }, update); - if (!result.matchedCount) { - throw new Error(`Static test user ${_id} not initialized`); + if (key === '__rooms') { + update.$set.__rooms = value ?? []; + continue; } - }), - ); + update.$set[key] = value; + } + + return { + updateOne: { + filter: { _id }, + update, + }, + }; + }); + + await usersCol.bulkWrite(operations); }; const makeLdapEntry = makeLdap; // preserve existing helper naming intent diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index cc78b75c1560e..d80542df81d05 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -132,30 +132,40 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { }; const configureStaticUsers = async (users: TestUserSeed[]) => { - await Promise.all( - users.map(async (user) => { - const setPayload: Partial = { - __rooms: user.extraRooms ?? [], - _updatedAt: new Date(), - }; - if (user.abacAttributes !== undefined) { - setPayload.abacAttributes = user.abacAttributes; - } - const update: { - $set: Partial; - $unset?: Record; - } = { - $set: setPayload, - }; - if (user.abacAttributes === undefined) { - update.$unset = { abacAttributes: 1 }; - } - const result = await usersCol.updateOne({ _id: user._id }, update); - if (result.matchedCount === 0) { - throw new Error(`Static test user ${user._id} not initialized`); - } - }), - ); + const operations = users.map((user) => { + const setPayload: Partial = { + __rooms: user.extraRooms ?? [], + _updatedAt: new Date(), + }; + + if (user.abacAttributes !== undefined) { + setPayload.abacAttributes = user.abacAttributes; + } + + const update: { + $set: Partial; + $unset?: Record; + } = { + $set: setPayload, + }; + + if (user.abacAttributes === undefined) { + update.$unset = { abacAttributes: 1 }; + } + + return { + updateOne: { + filter: { _id: user._id }, + update, + }, + }; + }); + + if (!operations.length) { + return; + } + + await usersCol.bulkWrite(operations); }; let debugSpy: jest.SpyInstance; From 1f13cb61a493d322a80e15e8f0386ab48d07e38f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 8 Jan 2026 10:42:05 -0600 Subject: [PATCH 9/9] user autoremoval --- .../abac/src/user-auto-removal.spec.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ee/packages/abac/src/user-auto-removal.spec.ts b/ee/packages/abac/src/user-auto-removal.spec.ts index d80542df81d05..a287ca14dfa49 100644 --- a/ee/packages/abac/src/user-auto-removal.spec.ts +++ b/ee/packages/abac/src/user-auto-removal.spec.ts @@ -168,6 +168,42 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { await usersCol.bulkWrite(operations); }; + // It's utterly incredible i have to do this so the tests are "fast" because mongo is not warm + // I could have increased the timeout for the first test too but... + const dbWarmup = async () => { + const uniqueSuffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + const warmupAttributeKey = `warmup_seed_${uniqueSuffix}`; + const warmupUserId = `warmup-abac-user-${uniqueSuffix}`; + const warmupRid = await insertRoom([]); + const subscriptionCol = db.collection('rocketchat_subscription'); + const seedSubscriptionId = `warmup-${uniqueSuffix}`; + + await subscriptionCol.insertOne({ + _id: seedSubscriptionId, + rid: `warmup-room-${uniqueSuffix}`, + u: { _id: `warmup-user-${uniqueSuffix}` }, + } as any); + await insertDefinitions([{ key: warmupAttributeKey, values: ['a'] }]); + await insertUsers([{ _id: warmupUserId, member: true, extraRooms: [warmupRid] }]); + await subscriptionCol.insertOne({ + _id: `warmup-sub-${uniqueSuffix}`, + rid: warmupRid, + u: { _id: warmupUserId }, + } as any); + + try { + await service.setRoomAbacAttributes(warmupRid, { [warmupAttributeKey]: ['a'] }, fakeActor); + } finally { + await roomsCol.deleteOne({ _id: warmupRid }); + await usersCol.deleteOne({ _id: warmupUserId }); + await subscriptionCol.deleteMany({ + _id: { $in: [seedSubscriptionId, `warmup-sub-${uniqueSuffix}`] }, + }); + await subscriptionCol.deleteMany({ rid: warmupRid }); + await db.collection('rocketchat_abac_attributes').deleteOne({ key: warmupAttributeKey }); + } + }; + let debugSpy: jest.SpyInstance; let auditSpy: jest.SpyInstance; @@ -182,6 +218,8 @@ describe('AbacService integration (onRoomAttributesChanged)', () => { usersCol = db.collection('users'); await usersCol.deleteMany({ _id: { $in: staticUserIds } }); await insertUsers(staticTestUsers); + + await dbWarmup(); }, 30_000); afterAll(async () => {