Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-8139 - add or remove guestrole according to room-membership updates #5382

Merged
merged 11 commits into from
Dec 6, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EntityId } from '@shared/domain/types';

export interface GroupFilter {
userId?: EntityId;
userIds?: EntityId[];
schoolId?: EntityId;
systemId?: EntityId;
groupTypes?: GroupTypes[];
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/group/repo/group.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class GroupRepo extends BaseDomainObjectRepo<Group, GroupEntity> {
public async findGroups(filter: GroupFilter, options?: IFindOptions<Group>): Promise<Page<Group>> {
const scope: GroupScope = new GroupScope();
scope.byUserId(filter.userId);
scope.byUserIds(filter.userIds);
scope.byOrganizationId(filter.schoolId);
scope.bySystemId(filter.systemId);

Expand Down
17 changes: 12 additions & 5 deletions apps/server/src/modules/group/repo/group.scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,42 @@ import { Scope } from '@shared/repo/scope';
import { GroupEntity, GroupEntityTypes } from '../entity';

export class GroupScope extends Scope<GroupEntity> {
byTypes(types: GroupEntityTypes[] | undefined): this {
public byTypes(types: GroupEntityTypes[] | undefined): this {
if (types) {
this.addQuery({ type: { $in: types } });
}
return this;
}

byOrganizationId(id: EntityId | undefined): this {
public byOrganizationId(id: EntityId | undefined): this {
if (id) {
this.addQuery({ organization: id });
}
return this;
}

bySystemId(id: EntityId | undefined): this {
public bySystemId(id: EntityId | undefined): this {
if (id) {
this.addQuery({ externalSource: { system: new ObjectId(id) } });
}
return this;
}

byUserId(id: EntityId | undefined): this {
public byUserId(id: EntityId | undefined): this {
if (id) {
this.addQuery({ users: { user: new ObjectId(id) } });
}
return this;
}

byNameQuery(nameQuery: string | undefined): this {
public byUserIds(ids: EntityId[] | undefined): this {
if (ids) {
this.addQuery({ users: { user: { $in: ids.map((id) => new ObjectId(id)) } } });
}
return this;
}

public byNameQuery(nameQuery: string | undefined): this {
if (nameQuery) {
const escapedName = nameQuery.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '').trim();
this.addQuery({ name: new RegExp(escapedName, 'i') });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { RoomMembershipDomainMapper } from './room-membership-domain.mapper';
export class RoomMembershipRepo {
constructor(private readonly em: EntityManager) {}

async findByRoomId(roomId: EntityId): Promise<RoomMembership | null> {
public async findByRoomId(roomId: EntityId): Promise<RoomMembership | null> {
const roomMembershipEntities = await this.em.findOne(RoomMembershipEntity, { roomId });
if (!roomMembershipEntities) return null;

Expand All @@ -19,28 +19,28 @@ export class RoomMembershipRepo {
return roomMemberships;
}

async findByRoomIds(roomIds: EntityId[]): Promise<RoomMembership[]> {
public async findByRoomIds(roomIds: EntityId[]): Promise<RoomMembership[]> {
const entities = await this.em.find(RoomMembershipEntity, { roomId: { $in: roomIds } });
const roomMemberships = entities.map((entity) => RoomMembershipDomainMapper.mapEntityToDo(entity));

return roomMemberships;
}

async findByGroupId(groupId: EntityId): Promise<RoomMembership[]> {
public async findByGroupId(groupId: EntityId): Promise<RoomMembership[]> {
const entities = await this.em.find(RoomMembershipEntity, { userGroupId: groupId });
const roomMemberships = entities.map((entity) => RoomMembershipDomainMapper.mapEntityToDo(entity));

return roomMemberships;
}

async findByGroupIds(groupIds: EntityId[]): Promise<RoomMembership[]> {
public async findByGroupIds(groupIds: EntityId[]): Promise<RoomMembership[]> {
const entities = await this.em.find(RoomMembershipEntity, { userGroupId: { $in: groupIds } });
const roomMemberships = entities.map((entity) => RoomMembershipDomainMapper.mapEntityToDo(entity));

return roomMemberships;
}

async save(roomMembership: RoomMembership | RoomMembership[]): Promise<void> {
public async save(roomMembership: RoomMembership | RoomMembership[]): Promise<void> {
const roomMemberships = Utils.asArray(roomMembership);

roomMemberships.forEach((member) => {
Expand All @@ -51,7 +51,7 @@ export class RoomMembershipRepo {
await this.em.flush();
}

async delete(roomMembership: RoomMembership | RoomMembership[]): Promise<void> {
public async delete(roomMembership: RoomMembership | RoomMembership[]): Promise<void> {
const roomMemberships = Utils.asArray(roomMembership);

roomMemberships.forEach((member) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { CqrsModule } from '@nestjs/cqrs';
import { AuthorizationModule } from '../authorization';
import { RoleModule } from '../role';
import { RoomModule } from '../room/room.module';
import { UserModule } from '../user';
import { RoomMembershipRule } from './authorization/room-membership.rule';
import { RoomMembershipRepo } from './repo/room-membership.repo';
import { RoomMembershipService } from './service/room-membership.service';

@Module({
imports: [AuthorizationModule, CqrsModule, GroupModule, RoleModule, RoomModule],
imports: [AuthorizationModule, CqrsModule, GroupModule, RoleModule, RoomModule, UserModule],
providers: [RoomMembershipService, RoomMembershipRepo, RoomMembershipRule],
exports: [RoomMembershipService],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { GroupService, GroupTypes } from '@modules/group';
import { RoleService } from '@modules/role';
import { MongoMemoryDatabaseModule } from '@infra/database';
import { Group, GroupService, GroupTypes } from '@modules/group';
import { RoleDto, RoleService } from '@modules/role';
import { RoomService } from '@modules/room/domain';
import { roomFactory } from '@modules/room/testing';
import { schoolFactory } from '@modules/school/testing';
import { UserService } from '@modules/user';
import { BadRequestException } from '@nestjs/common/exceptions';
import { Test, TestingModule } from '@nestjs/testing';
import { RoleName } from '@shared/domain/interface';
import { groupFactory, roleDtoFactory, userFactory } from '@shared/testing';
import { MongoMemoryDatabaseModule } from '@src/infra/database';
import { RoomService } from '@src/modules/room/domain';
import { groupFactory, roleDtoFactory, roleFactory, userDoFactory, userFactory } from '@shared/testing';
import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do';
import { RoomMembershipRepo } from '../repo/room-membership.repo';
import { roomMembershipFactory } from '../testing';
Expand All @@ -20,6 +22,7 @@ describe('RoomMembershipService', () => {
let groupService: DeepMocked<GroupService>;
let roleService: DeepMocked<RoleService>;
let roomService: DeepMocked<RoomService>;
let userService: DeepMocked<UserService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand All @@ -42,6 +45,10 @@ describe('RoomMembershipService', () => {
provide: RoomService,
useValue: createMock<RoomService>(),
},
{
provide: UserService,
useValue: createMock<UserService>(),
},
],
}).compile();

Expand All @@ -50,6 +57,7 @@ describe('RoomMembershipService', () => {
groupService = module.get(GroupService);
roleService = module.get(RoleService);
roomService = module.get(RoomService);
userService = module.get(UserService);
});

afterAll(async () => {
Expand Down Expand Up @@ -113,9 +121,14 @@ describe('RoomMembershipService', () => {
describe('when roomMembership exists', () => {
const setup = () => {
const user = userFactory.buildWithId();
const group = groupFactory.build({ type: GroupTypes.ROOM });
const room = roomFactory.build();
const roomMembership = roomMembershipFactory.build({ roomId: room.id, userGroupId: group.id });
const school = schoolFactory.build();
const group = groupFactory.build({ type: GroupTypes.ROOM, organizationId: school.id });
const room = roomFactory.build({ schoolId: school.id });
const roomMembership = roomMembershipFactory.build({
roomId: room.id,
userGroupId: group.id,
schoolId: school.id,
});

roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership);

Expand All @@ -136,6 +149,14 @@ describe('RoomMembershipService', () => {
{ userId: user.id, roleName: RoleName.ROOMEDITOR },
]);
});

it('should add user to school', async () => {
const { user, room } = setup();

await service.addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOMEDITOR }]);

expect(userService.addSecondarySchoolToUsers).toHaveBeenCalledWith([user.id], room.schoolId);
});
});
});

Expand Down Expand Up @@ -176,6 +197,7 @@ describe('RoomMembershipService', () => {

roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership);
groupService.findById.mockResolvedValue(group);
groupService.findGroups.mockResolvedValue({ total: 1, data: [group] });

return {
user,
Expand All @@ -193,6 +215,94 @@ describe('RoomMembershipService', () => {
expect(groupService.removeUsersFromGroup).toHaveBeenCalledWith(group.id, [user.id]);
});
});

const setupUserWithSecondarySchool = () => {
const secondarySchool = schoolFactory.build();
const otherSchool = schoolFactory.build();
const role = roleFactory.buildWithId({ name: RoleName.TEACHER });
const guestTeacher = roleFactory.buildWithId({ name: RoleName.GUESTTEACHER });
const externalUser = userDoFactory.buildWithId({
roles: [role],
secondarySchools: [{ schoolId: secondarySchool.id, role: new RoleDto(guestTeacher) }],
});

return { secondarySchool, externalUser, otherSchool };
};

const setupGroupAndRoom = (schoolId: string) => {
const group = groupFactory.build({ type: GroupTypes.ROOM });
const room = roomFactory.build({ schoolId });
const roomMembership = roomMembershipFactory.build({
roomId: room.id,
userGroupId: group.id,
schoolId,
});

return { group, room, roomMembership };
};

const mockGroupsAtSchoolAfterRemoval = (groups: Group[]) => {
groupService.findGroups.mockResolvedValue({ total: groups.length, data: groups });
};

it('should pass the schoolId of the room', async () => {
const { secondarySchool, externalUser } = setupUserWithSecondarySchool();

const roomEditorRole = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR });

const { room, group, roomMembership } = setupGroupAndRoom(secondarySchool.id);
group.addUser({ userId: externalUser.id as string, roleId: roomEditorRole.id });

roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership);
groupService.findById.mockResolvedValue(group);
groupService.removeUsersFromGroup.mockResolvedValue(group);
mockGroupsAtSchoolAfterRemoval([]);

await service.removeMembersFromRoom(room.id, [externalUser.id as string]);

expect(groupService.findGroups).toHaveBeenCalledWith(expect.objectContaining({ schoolId: secondarySchool.id }));
});

describe('when after removal: user is not in any room of that secondary school', () => {
it('should remove user from secondary school', async () => {
const { secondarySchool, externalUser } = setupUserWithSecondarySchool();

const { room, group, roomMembership } = setupGroupAndRoom(secondarySchool.id);
const roomEditorRole = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR });
group.addUser({ userId: externalUser.id as string, roleId: roomEditorRole.id });

roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership);
groupService.findById.mockResolvedValue(group);
groupService.removeUsersFromGroup.mockResolvedValue(group);
mockGroupsAtSchoolAfterRemoval([]);

await service.removeMembersFromRoom(room.id, [externalUser.id as string]);

expect(userService.removeSecondarySchoolFromUsers).toHaveBeenCalledWith([externalUser.id], secondarySchool.id);
});
});

describe('when after removal: user is still in a room of that secondary school', () => {
it('should not remove user from secondary school', async () => {
const { secondarySchool, externalUser } = setupUserWithSecondarySchool();

const roomEditorRole = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR });

const { room, group, roomMembership } = setupGroupAndRoom(secondarySchool.id);
group.addUser({ userId: externalUser.id as string, roleId: roomEditorRole.id });
const { group: group2 } = setupGroupAndRoom(secondarySchool.id);
group2.addUser({ userId: externalUser.id as string, roleId: roomEditorRole.id });

roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership);
groupService.findById.mockResolvedValue(group);
groupService.removeUsersFromGroup.mockResolvedValue(group);
mockGroupsAtSchoolAfterRemoval([group2]);

await service.removeMembersFromRoom(room.id, [externalUser.id as string]);

expect(userService.removeSecondarySchoolFromUsers).not.toHaveBeenCalled();
});
});
});

describe('deleteRoomMembership', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { Group, GroupService, GroupTypes } from '@modules/group';
import { RoleDto, RoleService } from '@modules/role';
import { UserService } from '@modules/user';
import { BadRequestException, Injectable } from '@nestjs/common';
import { RoleName } from '@shared/domain/interface';
import { EntityId } from '@shared/domain/types';
Expand All @@ -15,14 +16,15 @@ export class RoomMembershipService {
private readonly groupService: GroupService,
private readonly roomMembershipRepo: RoomMembershipRepo,
private readonly roleService: RoleService,
private readonly roomService: RoomService
private readonly roomService: RoomService,
private readonly userService: UserService
) {}

private async createNewRoomMembership(
roomId: EntityId,
userId: EntityId,
roleName: RoleName.ROOMEDITOR | RoleName.ROOMVIEWER
) {
): Promise<RoomMembership> {
const room = await this.roomService.getSingleRoom(roomId);

const group = await this.groupService.createGroup(
Expand Down Expand Up @@ -65,7 +67,7 @@ export class RoomMembershipService {
return roomMembershipAuthorizable;
}

public async deleteRoomMembership(roomId: EntityId) {
public async deleteRoomMembership(roomId: EntityId): Promise<void> {
const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId);
if (roomMembership === null) return;

Expand All @@ -90,6 +92,9 @@ export class RoomMembershipService {

await this.groupService.addUsersToGroup(roomMembership.userGroupId, userIdsAndRoles);

const userIds = userIdsAndRoles.map((user) => user.userId);
await this.userService.addSecondarySchoolToUsers(userIds, roomMembership.schoolId);

return roomMembership.id;
}

Expand All @@ -101,6 +106,8 @@ export class RoomMembershipService {

const group = await this.groupService.findById(roomMembership.userGroupId);
await this.groupService.removeUsersFromGroup(group.id, userIds);

await this.handleGuestRoleRemoval(userIds, roomMembership.schoolId);
}

public async getRoomMembershipAuthorizablesByUserId(userId: EntityId): Promise<RoomMembershipAuthorizable[]> {
Expand Down Expand Up @@ -141,4 +148,15 @@ export class RoomMembershipService {

return roomMembershipAuthorizable;
}

private async handleGuestRoleRemoval(userIds: EntityId[], schoolId: EntityId): Promise<void> {
const { data: groups } = await this.groupService.findGroups({ userIds, groupTypes: [GroupTypes.ROOM], schoolId });

const userIdsInGroups = groups.flatMap((group) => group.users.map((groupUser) => groupUser.userId));
const removeUserIds = userIds.filter((userId) => !userIdsInGroups.includes(userId));

if (removeUserIds.length > 0) {
await this.userService.removeSecondarySchoolFromUsers(removeUserIds, schoolId);
}
}
}
Loading
Loading