diff --git a/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts b/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts index daa4985498d..148e1c71e8e 100644 --- a/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts +++ b/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts @@ -3,6 +3,7 @@ export const enum DeletionDomainModel { CLASS = 'class', COURSEGROUP = 'courseGroup', COURSE = 'course', + DASHBOARD = 'dashboard', FILE = 'file', LESSONS = 'lessons', PSEUDONYMS = 'pseudonyms', diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts index 9e2f2604f16..feea12da31e 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts @@ -3,7 +3,7 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { setupEntities, userDoFactory } from '@shared/testing'; import { AccountService } from '@modules/account'; import { ClassService } from '@modules/class'; -import { CourseGroupService, CourseService } from '@modules/learnroom'; +import { CourseGroupService, CourseService, DashboardService } from '@modules/learnroom'; import { FilesService } from '@modules/files'; import { LessonService } from '@modules/lesson'; import { PseudonymService } from '@modules/pseudonym'; @@ -40,6 +40,7 @@ describe(DeletionRequestUc.name, () => { let rocketChatUserService: DeepMocked; let rocketChatService: DeepMocked; let registrationPinService: DeepMocked; + let dashboardService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -105,6 +106,10 @@ describe(DeletionRequestUc.name, () => { provide: RegistrationPinService, useValue: createMock(), }, + { + provide: DashboardService, + useValue: createMock(), + }, ], }).compile(); @@ -123,6 +128,7 @@ describe(DeletionRequestUc.name, () => { rocketChatUserService = module.get(RocketChatUserService); rocketChatService = module.get(RocketChatService); registrationPinService = module.get(RegistrationPinService); + dashboardService = module.get(DashboardService); await setupEntities(); }); @@ -199,6 +205,7 @@ describe(DeletionRequestUc.name, () => { teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); userService.deleteUser.mockResolvedValueOnce(1); rocketChatUserService.deleteByUserId.mockResolvedValueOnce(1); + dashboardService.deleteDashboardByUserId.mockResolvedValueOnce(1); return { deletionRequestToExecute, @@ -381,6 +388,16 @@ describe(DeletionRequestUc.name, () => { expect(rocketChatService.deleteUser).toHaveBeenCalledWith(rocketChatUser.username); }); + it('should call dashboardService.deleteDashboardByUserId to delete USERS DASHBOARD', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(dashboardService.deleteDashboardByUserId).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + it('should call deletionLogService.createDeletionLog to create logs for deletionRequest', async () => { const { deletionRequestToExecute } = setup(); @@ -388,7 +405,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(10); + expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(11); }); }); diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts index 100029887ca..afb56be3193 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -1,7 +1,7 @@ import { AccountService } from '@modules/account/services'; import { ClassService } from '@modules/class'; import { FilesService } from '@modules/files/service'; -import { CourseGroupService, CourseService } from '@modules/learnroom/service'; +import { CourseGroupService, CourseService, DashboardService } from '@modules/learnroom'; import { LessonService } from '@modules/lesson/service'; import { PseudonymService } from '@modules/pseudonym'; import { RegistrationPinService } from '@modules/registration-pin'; @@ -36,7 +36,8 @@ export class DeletionRequestUc { private readonly rocketChatUserService: RocketChatUserService, private readonly rocketChatService: RocketChatService, private readonly logger: LegacyLogger, - private readonly registrationPinService: RegistrationPinService + private readonly registrationPinService: RegistrationPinService, + private readonly dashboardService: DashboardService ) { this.logger.setContext(DeletionRequestUc.name); } @@ -105,6 +106,7 @@ export class DeletionRequestUc { this.removeUser(deletionRequest), this.removeUserFromRocketChat(deletionRequest), this.removeUserRegistrationPin(deletionRequest), + this.removeUsersDashboard(deletionRequest), ]); await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); } catch (error) { @@ -198,6 +200,19 @@ export class DeletionRequestUc { ); } + private async removeUsersDashboard(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUsersDashboard', deletionRequest }); + + const dashboardDeleted: number = await this.dashboardService.deleteDashboardByUserId(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.DASHBOARD, + DeletionOperationModel.DELETE, + 0, + dashboardDeleted + ); + } + private async removeUsersFilesAndPermissions(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUsersFilesAndPermissions', deletionRequest }); diff --git a/apps/server/src/modules/learnroom/index.ts b/apps/server/src/modules/learnroom/index.ts index 6c28dcb2e95..94cbf86ff33 100644 --- a/apps/server/src/modules/learnroom/index.ts +++ b/apps/server/src/modules/learnroom/index.ts @@ -5,4 +5,5 @@ export { CourseService, RoomsService, CourseGroupService, + DashboardService, } from './service'; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 9c9d4d80036..f78d74dfafa 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -3,7 +3,15 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; import { Module } from '@nestjs/common'; -import { BoardRepo, CourseGroupRepo, CourseRepo, DashboardModelMapper, DashboardRepo, UserRepo } from '@shared/repo'; +import { + BoardRepo, + CourseGroupRepo, + CourseRepo, + DashboardElementRepo, + DashboardModelMapper, + DashboardRepo, + UserRepo, +} from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { BoardCopyService, @@ -12,6 +20,7 @@ import { CourseCopyService, CourseGroupService, CourseService, + DashboardService, RoomsService, } from './service'; @@ -22,6 +31,7 @@ import { provide: 'DASHBOARD_REPO', useClass: DashboardRepo, }, + DashboardElementRepo, DashboardModelMapper, CourseRepo, BoardRepo, @@ -34,7 +44,15 @@ import { ColumnBoardTargetService, CourseGroupService, CourseGroupRepo, + DashboardService, + ], + exports: [ + CourseCopyService, + CourseService, + RoomsService, + CommonCartridgeExportService, + CourseGroupService, + DashboardService, ], - exports: [CourseCopyService, CourseService, RoomsService, CommonCartridgeExportService, CourseGroupService], }) export class LearnroomModule {} diff --git a/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts b/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts new file mode 100644 index 00000000000..00f8207e23c --- /dev/null +++ b/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts @@ -0,0 +1,118 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DashboardElementRepo, IDashboardRepo, UserRepo } from '@shared/repo'; +import { setupEntities, userFactory } from '@shared/testing'; +import { LearnroomMetadata, LearnroomTypes } from '@shared/domain/types'; +import { DashboardEntity, GridElement } from '@shared/domain/entity'; +import { DashboardService } from '.'; + +const learnroomMock = (id: string, name: string) => { + return { + getMetadata(): LearnroomMetadata { + return { + id, + type: LearnroomTypes.Course, + title: name, + shortTitle: name.substr(0, 2), + displayColor: '#ACACAC', + }; + }, + }; +}; + +describe(DashboardService.name, () => { + let module: TestingModule; + let userRepo: DeepMocked; + let dashboardRepo: IDashboardRepo; + let dashboardElementRepo: DeepMocked; + let dashboardService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + module = await Test.createTestingModule({ + providers: [ + DashboardService, + { + provide: UserRepo, + useValue: createMock(), + }, + { + provide: 'DASHBOARD_REPO', + useValue: createMock(), + }, + { + provide: DashboardElementRepo, + useValue: createMock(), + }, + ], + }).compile(); + dashboardService = module.get(DashboardService); + userRepo = module.get(UserRepo); + dashboardRepo = module.get('DASHBOARD_REPO'); + dashboardElementRepo = module.get(DashboardElementRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when deleting by userId', () => { + const setup = () => { + const user = userFactory.buildWithId(); + userRepo.findById.mockResolvedValue(user); + + return { user }; + }; + + it('should call dashboardRepo.getUsersDashboard', async () => { + const { user } = setup(); + const spy = jest.spyOn(dashboardRepo, 'getUsersDashboard'); + + await dashboardService.deleteDashboardByUserId(user.id); + + expect(spy).toHaveBeenCalledWith(user.id); + }); + + it('should call dashboardElementRepo.deleteByDashboardId', async () => { + const { user } = setup(); + jest.spyOn(dashboardRepo, 'getUsersDashboard').mockResolvedValueOnce( + new DashboardEntity('dashboardId', { + grid: [ + { + pos: { x: 1, y: 2 }, + gridElement: GridElement.FromPersistedReference('elementId', learnroomMock('referenceId', 'Mathe')), + }, + ], + userId: 'userId', + }) + ); + const spy = jest.spyOn(dashboardElementRepo, 'deleteByDashboardId'); + + await dashboardService.deleteDashboardByUserId(user.id); + + expect(spy).toHaveBeenCalledWith('dashboardId'); + }); + + it('should call dashboardRepo.deleteDashboardByUserId', async () => { + const { user } = setup(); + const spy = jest.spyOn(dashboardRepo, 'deleteDashboardByUserId'); + + await dashboardService.deleteDashboardByUserId(user.id); + + expect(spy).toHaveBeenCalledWith(user.id); + }); + + it('should delete users dashboard', async () => { + const { user } = setup(); + jest.spyOn(dashboardRepo, 'deleteDashboardByUserId').mockImplementation(() => Promise.resolve(1)); + + const result = await dashboardService.deleteDashboardByUserId(user.id); + + expect(result).toEqual(1); + }); + }); +}); diff --git a/apps/server/src/modules/learnroom/service/dashboard.service.ts b/apps/server/src/modules/learnroom/service/dashboard.service.ts new file mode 100644 index 00000000000..4a7910f0991 --- /dev/null +++ b/apps/server/src/modules/learnroom/service/dashboard.service.ts @@ -0,0 +1,19 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { IDashboardRepo, DashboardElementRepo } from '@shared/repo'; + +@Injectable() +export class DashboardService { + constructor( + @Inject('DASHBOARD_REPO') private readonly dashboardRepo: IDashboardRepo, + private readonly dashboardElementRepo: DashboardElementRepo + ) {} + + async deleteDashboardByUserId(userId: EntityId): Promise { + const usersDashboard = await this.dashboardRepo.getUsersDashboard(userId); + await this.dashboardElementRepo.deleteByDashboardId(usersDashboard.id); + const result = await this.dashboardRepo.deleteDashboardByUserId(userId); + + return result; + } +} diff --git a/apps/server/src/modules/learnroom/service/index.ts b/apps/server/src/modules/learnroom/service/index.ts index ca9d75634cf..f5e9336abcf 100644 --- a/apps/server/src/modules/learnroom/service/index.ts +++ b/apps/server/src/modules/learnroom/service/index.ts @@ -5,3 +5,4 @@ export * from './common-cartridge-export.service'; export * from './course.service'; export * from './rooms.service'; export * from './coursegroup.service'; +export * from './dashboard.service'; diff --git a/apps/server/src/shared/repo/dashboard/dashboard.repo.integration.spec.ts b/apps/server/src/shared/repo/dashboard/dashboard.repo.integration.spec.ts index 82e4c2dd03d..d82502ab351 100644 --- a/apps/server/src/shared/repo/dashboard/dashboard.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/dashboard/dashboard.repo.integration.spec.ts @@ -244,4 +244,46 @@ describe('dashboard repo', () => { }); }); }); + + describe('deleteDashboardByUserId', () => { + const setup = async () => { + const userWithoutDashoard = userFactory.build(); + const user = userFactory.build(); + const course = courseFactory.build({ students: [user], name: 'Mathe' }); + await em.persistAndFlush([userWithoutDashoard, user, course]); + const dashboard = new DashboardEntity(new ObjectId().toString(), { + grid: [ + { + pos: { x: 1, y: 3 }, + gridElement: GridElement.FromSingleReference(course), + }, + ], + userId: user.id, + }); + await repo.persistAndFlush(dashboard); + + return { userWithoutDashoard, user }; + }; + describe('when user has no dashboard ', () => { + it('should return 0', async () => { + const { userWithoutDashoard } = await setup(); + + const result = await repo.deleteDashboardByUserId(userWithoutDashoard.id); + expect(result).toEqual(0); + }); + }); + + describe('when user has dashboard ', () => { + it('should return 1', async () => { + const { user } = await setup(); + + const result1 = await repo.deleteDashboardByUserId(user.id); + expect(result1).toEqual(1); + + const result2 = await repo.getUsersDashboard(user.id); + expect(result2 instanceof DashboardEntity).toEqual(true); + expect(result2.getGrid().length).toEqual(0); + }); + }); + }); }); diff --git a/apps/server/src/shared/repo/dashboard/dashboard.repo.ts b/apps/server/src/shared/repo/dashboard/dashboard.repo.ts index 925a7760eba..2ddb7599921 100644 --- a/apps/server/src/shared/repo/dashboard/dashboard.repo.ts +++ b/apps/server/src/shared/repo/dashboard/dashboard.repo.ts @@ -15,6 +15,7 @@ export interface IDashboardRepo { getUsersDashboard(userId: EntityId): Promise; getDashboardById(id: EntityId): Promise; persistAndFlush(entity: DashboardEntity): Promise; + deleteDashboardByUserId(userId: EntityId): Promise; } @Injectable() @@ -51,4 +52,10 @@ export class DashboardRepo implements IDashboardRepo { return dashboard; } + + async deleteDashboardByUserId(userId: EntityId): Promise { + const promise: Promise = this.em.nativeDelete(DashboardModelEntity, { user: userId }); + + return promise; + } } diff --git a/apps/server/src/shared/repo/dashboard/dashboardElement.repo.spec.ts b/apps/server/src/shared/repo/dashboard/dashboardElement.repo.spec.ts new file mode 100644 index 00000000000..7c416193b2b --- /dev/null +++ b/apps/server/src/shared/repo/dashboard/dashboardElement.repo.spec.ts @@ -0,0 +1,91 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DashboardGridElementModel, DashboardModelEntity } from '@shared/domain/entity'; +import { courseFactory, userFactory } from '@shared/testing'; +import { DashboardElementRepo } from './dashboardElement.repo'; + +describe(DashboardElementRepo.name, () => { + let repo: DashboardElementRepo; + let em: EntityManager; + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [DashboardElementRepo], + }).compile(); + + repo = module.get(DashboardElementRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('defined', () => { + it('repo should be defined', () => { + expect(repo).toBeDefined(); + expect(typeof repo.deleteByDashboardId).toEqual('function'); + }); + + it('entity manager should be defined', () => { + expect(em).toBeDefined(); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(DashboardGridElementModel); + }); + }); + + describe('deleteByDasboardId', () => { + const setup = async () => { + const user1 = userFactory.build(); + const user2 = userFactory.build(); + const course = courseFactory.build({ students: [user1], name: 'Mathe' }); + await em.persistAndFlush([user1, course]); + + const dashboard = new DashboardModelEntity({ id: new ObjectId().toString(), user: user1 }); + const dashboardWithoutDashboardElement = new DashboardModelEntity({ id: new ObjectId().toString(), user: user2 }); + + const element = new DashboardGridElementModel({ + id: new ObjectId().toString(), + xPos: 1, + yPos: 2, + references: [course], + dashboard, + }); + + dashboard.gridElements.add(element); + + await em.persistAndFlush([dashboard, dashboardWithoutDashboardElement]); + em.clear(); + + return { dashboard, dashboardWithoutDashboardElement }; + }; + + describe('when user has no dashboardElement ', () => { + it('should return 0', async () => { + const { dashboardWithoutDashboardElement } = await setup(); + + const result = await repo.deleteByDashboardId(dashboardWithoutDashboardElement.id); + expect(result).toEqual(0); + }); + }); + + describe('when user has some dashboardElement on dashboard ', () => { + it('should return 1', async () => { + const { dashboard } = await setup(); + + const result1 = await repo.deleteByDashboardId(dashboard.id); + expect(result1).toEqual(1); + + const result2 = await em.findOne(DashboardGridElementModel, { + dashboard: dashboard.id, + }); + expect(result2).toEqual(null); + }); + }); + }); +}); diff --git a/apps/server/src/shared/repo/dashboard/dashboardElement.repo.ts b/apps/server/src/shared/repo/dashboard/dashboardElement.repo.ts new file mode 100644 index 00000000000..27f5baf93f2 --- /dev/null +++ b/apps/server/src/shared/repo/dashboard/dashboardElement.repo.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/core'; +import { DashboardGridElementModel } from '@shared/domain/entity'; +import { EntityId } from '@shared/domain/types'; +import { ObjectId } from 'bson'; + +@Injectable() +export class DashboardElementRepo { + constructor(private readonly em: EntityManager) {} + + get entityName() { + return DashboardGridElementModel; + } + + async deleteByDashboardId(id: EntityId): Promise { + const promise = this.em.nativeDelete(DashboardGridElementModel, { + dashboard: new ObjectId(id), + }); + + return promise; + } +} diff --git a/apps/server/src/shared/repo/dashboard/index.ts b/apps/server/src/shared/repo/dashboard/index.ts index 7802371f925..ff7c943af5d 100644 --- a/apps/server/src/shared/repo/dashboard/index.ts +++ b/apps/server/src/shared/repo/dashboard/index.ts @@ -1,2 +1,3 @@ export * from './dashboard.repo'; +export * from './dashboardElement.repo'; export * from './dashboard.model.mapper';