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 1a4f3bcf425..daa4985498d 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 @@ -6,6 +6,7 @@ export const enum DeletionDomainModel { FILE = 'file', LESSONS = 'lessons', PSEUDONYMS = 'pseudonyms', + REGISTRATIONPIN = 'registrationPin', ROCKETCHATUSER = 'rocketChatUser', TEAMS = 'teams', USER = 'user', 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 34c34e302f5..69ec72a0db5 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 @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { setupEntities } from '@shared/testing'; +import { setupEntities, userDoFactory } from '@shared/testing'; import { AccountService } from '@modules/account/services'; import { ClassService } from '@modules/class'; import { CourseGroupService, CourseService } from '@modules/learnroom/service'; @@ -12,6 +12,7 @@ import { UserService } from '@modules/user'; import { RocketChatService } from '@modules/rocketchat'; import { rocketChatUserFactory } from '@modules/rocketchat-user/domain/testing'; import { RocketChatUser, RocketChatUserService } from '@modules/rocketchat-user'; +import { RegistrationPinService } from '@modules/registration-pin'; import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; import { DeletionLogService } from '../services/deletion-log.service'; import { DeletionRequestService } from '../services'; @@ -37,6 +38,7 @@ describe(DeletionRequestUc.name, () => { let userService: DeepMocked; let rocketChatUserService: DeepMocked; let rocketChatService: DeepMocked; + let registrationPinService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -94,6 +96,10 @@ describe(DeletionRequestUc.name, () => { provide: RocketChatService, useValue: createMock(), }, + { + provide: RegistrationPinService, + useValue: createMock(), + }, ], }).compile(); @@ -111,6 +117,7 @@ describe(DeletionRequestUc.name, () => { userService = module.get(UserService); rocketChatUserService = module.get(RocketChatUserService); rocketChatService = module.get(RocketChatService); + registrationPinService = module.get(RegistrationPinService); await setupEntities(); }); @@ -168,10 +175,13 @@ describe(DeletionRequestUc.name, () => { const setup = () => { jest.clearAllMocks(); const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); + const user = userDoFactory.buildWithId(); const rocketChatUser: RocketChatUser = rocketChatUserFactory.build({ userId: deletionRequestToExecute.targetRefId, }); + const parentEmail = 'parent@parent.eu'; + registrationPinService.deleteRegistrationPinByEmail.mockResolvedValueOnce(2); classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2); @@ -186,6 +196,8 @@ describe(DeletionRequestUc.name, () => { return { deletionRequestToExecute, rocketChatUser, + user, + parentEmail, }; }; @@ -215,6 +227,29 @@ describe(DeletionRequestUc.name, () => { expect(accountService.deleteByUserId).toHaveBeenCalled(); }); + it('should call registrationPinService.deleteRegistrationPinByEmail to delete user data in registrationPin module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(registrationPinService.deleteRegistrationPinByEmail).toHaveBeenCalled(); + }); + + it('should call userService.getParentEmailsFromUser to get parentEmails', async () => { + const { deletionRequestToExecute, user, parentEmail } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + userService.findById.mockResolvedValueOnce(user); + userService.getParentEmailsFromUser.mockRejectedValue([parentEmail]); + registrationPinService.deleteRegistrationPinByEmail.mockRejectedValueOnce(2); + + await uc.executeDeletionRequests(); + + expect(userService.getParentEmailsFromUser).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + it('should call classService.deleteUserDataFromClasses to delete user data in class module', async () => { const { deletionRequestToExecute } = setup(); 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 abea56fda96..7bacc428310 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -10,6 +10,7 @@ import { FilesService } from '@modules/files/service'; import { AccountService } from '@modules/account/services'; import { RocketChatUserService } from '@modules/rocketchat-user'; import { RocketChatService } from '@modules/rocketchat'; +import { RegistrationPinService } from '@modules/registration-pin'; import { DeletionRequestService } from '../services/deletion-request.service'; import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; import { DeletionLogService } from '../services/deletion-log.service'; @@ -42,7 +43,8 @@ export class DeletionRequestUc { private readonly teamService: TeamService, private readonly userService: UserService, private readonly rocketChatUserService: RocketChatUserService, - private readonly rocketChatService: RocketChatService + private readonly rocketChatService: RocketChatService, + private readonly registrationPinService: RegistrationPinService ) {} async createDeletionRequest(deletionRequest: DeletionRequestProps): Promise { @@ -101,6 +103,7 @@ export class DeletionRequestUc { this.removeUserFromTeams(deletionRequest), this.removeUser(deletionRequest), this.removeUserFromRocketChat(deletionRequest), + this.removeUserRegistrationPin(deletionRequest), ]); await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); } catch (error) { @@ -131,6 +134,25 @@ export class DeletionRequestUc { await this.logDeletion(deletionRequest, DeletionDomainModel.ACCOUNT, DeletionOperationModel.DELETE, 0, 1); } + private async removeUserRegistrationPin(deletionRequest: DeletionRequest) { + const userToDeletion = await this.userService.findById(deletionRequest.targetRefId); + const parentEmails = await this.userService.getParentEmailsFromUser(deletionRequest.targetRefId); + const emailsToDeletion: string[] = [userToDeletion.email, ...parentEmails]; + + const result = await Promise.all( + emailsToDeletion.map((email) => this.registrationPinService.deleteRegistrationPinByEmail(email)) + ); + const deletedRegistrationPin = result.filter((res) => res !== 0).length; + + await this.logDeletion( + deletionRequest, + DeletionDomainModel.REGISTRATIONPIN, + DeletionOperationModel.DELETE, + 0, + deletedRegistrationPin + ); + } + private async removeUserFromClasses(deletionRequest: DeletionRequest) { const classesUpdated: number = await this.classService.deleteUserDataFromClasses(deletionRequest.targetRefId); await this.logDeletion( diff --git a/apps/server/src/modules/registration-pin/entity/index.ts b/apps/server/src/modules/registration-pin/entity/index.ts new file mode 100644 index 00000000000..ed20550896f --- /dev/null +++ b/apps/server/src/modules/registration-pin/entity/index.ts @@ -0,0 +1 @@ +export * from './registration-pin.entity'; diff --git a/apps/server/src/modules/registration-pin/entity/registration-pin.entity.spec.ts b/apps/server/src/modules/registration-pin/entity/registration-pin.entity.spec.ts new file mode 100644 index 00000000000..c8570e8d1b2 --- /dev/null +++ b/apps/server/src/modules/registration-pin/entity/registration-pin.entity.spec.ts @@ -0,0 +1,57 @@ +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { RegistrationPinEntity } from '.'; + +describe(RegistrationPinEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + email: 'test@test.eu', + pin: 'test123', + verified: false, + importHash: '02a00804nnQbLbCDEMVuk56pzZ3A0SC2cYnmM9cyY25IVOnf0K3YCKqW6zxC', + }; + + return { props }; + }; + + describe('constructor', () => { + describe('When constructor is called', () => { + it('should throw an error by empty constructor', () => { + // @ts-expect-error: Test case + const test = () => new RegistrationPinEntity(); + expect(test).toThrow(); + }); + + it('should create a registrationPins by passing required properties', () => { + const { props } = setup(); + const entity: RegistrationPinEntity = new RegistrationPinEntity(props); + + expect(entity instanceof RegistrationPinEntity).toEqual(true); + }); + + it(`should return a valid object with fields values set from the provided complete props object`, () => { + const { props } = setup(); + const entity: RegistrationPinEntity = new RegistrationPinEntity(props); + + const entityProps = { + id: entity.id, + email: entity.email, + pin: entity.pin, + verified: entity.verified, + importHash: entity.importHash, + }; + + expect(entityProps).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/registration-pin/entity/registration-pin.entity.ts b/apps/server/src/modules/registration-pin/entity/registration-pin.entity.ts new file mode 100644 index 00000000000..ee5ece7a421 --- /dev/null +++ b/apps/server/src/modules/registration-pin/entity/registration-pin.entity.ts @@ -0,0 +1,40 @@ +import { Entity, Index, Property } from '@mikro-orm/core'; +import { EntityId } from '@shared/domain'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; + +export interface RegistrationPinEntityProps { + id?: EntityId; + email: string; + pin: string; + verified: boolean; + importHash: string; +} + +@Entity({ tableName: 'registrationpins' }) +@Index({ properties: ['email', 'pin'] }) +export class RegistrationPinEntity extends BaseEntityWithTimestamps { + @Property() + @Index() + email: string; + + @Property() + pin: string; + + @Property({ default: false }) + verified: boolean; + + @Property() + @Index() + importHash: string; + + constructor(props: RegistrationPinEntityProps) { + super(); + if (props.id !== undefined) { + this.id = props.id; + } + this.email = props.email; + this.pin = props.pin; + this.verified = props.verified; + this.importHash = props.importHash; + } +} diff --git a/apps/server/src/modules/registration-pin/entity/testing/factory/index.ts b/apps/server/src/modules/registration-pin/entity/testing/factory/index.ts new file mode 100644 index 00000000000..74b1134fc78 --- /dev/null +++ b/apps/server/src/modules/registration-pin/entity/testing/factory/index.ts @@ -0,0 +1 @@ +export * from './registration-pin.entity.factory'; diff --git a/apps/server/src/modules/registration-pin/entity/testing/factory/registration-pin.entity.factory.ts b/apps/server/src/modules/registration-pin/entity/testing/factory/registration-pin.entity.factory.ts new file mode 100644 index 00000000000..9a162147bed --- /dev/null +++ b/apps/server/src/modules/registration-pin/entity/testing/factory/registration-pin.entity.factory.ts @@ -0,0 +1,18 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { RegistrationPinEntity, RegistrationPinEntityProps } from '../../registration-pin.entity'; + +export const registrationPinEntityFactory = BaseFactory.define( + RegistrationPinEntity, + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + email: `name-${sequence}@schul-cloud.org`, + pin: `123-${sequence}`, + verified: false, + importHash: `importHash-${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + }; + } +); diff --git a/apps/server/src/modules/registration-pin/entity/testing/index.ts b/apps/server/src/modules/registration-pin/entity/testing/index.ts new file mode 100644 index 00000000000..d847d7abce6 --- /dev/null +++ b/apps/server/src/modules/registration-pin/entity/testing/index.ts @@ -0,0 +1 @@ +export * from './factory'; diff --git a/apps/server/src/modules/registration-pin/index.ts b/apps/server/src/modules/registration-pin/index.ts new file mode 100644 index 00000000000..89a77b2fa2c --- /dev/null +++ b/apps/server/src/modules/registration-pin/index.ts @@ -0,0 +1,2 @@ +export * from './registration-pin.module'; +export { RegistrationPinService } from './service'; diff --git a/apps/server/src/modules/registration-pin/registration-pin.module.ts b/apps/server/src/modules/registration-pin/registration-pin.module.ts new file mode 100644 index 00000000000..76fa8716c94 --- /dev/null +++ b/apps/server/src/modules/registration-pin/registration-pin.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; +import { RegistrationPinService } from './service'; +import { RegistrationPinRepo } from './repo'; + +@Module({ + imports: [LoggerModule], + providers: [RegistrationPinService, RegistrationPinRepo], + exports: [RegistrationPinService], +}) +export class RegistrationPinModule {} diff --git a/apps/server/src/modules/registration-pin/repo/index.ts b/apps/server/src/modules/registration-pin/repo/index.ts new file mode 100644 index 00000000000..e32bd34f567 --- /dev/null +++ b/apps/server/src/modules/registration-pin/repo/index.ts @@ -0,0 +1 @@ +export * from './registration-pin.repo'; diff --git a/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts new file mode 100644 index 00000000000..c357351fa37 --- /dev/null +++ b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts @@ -0,0 +1,64 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { cleanupCollections, userFactory } from '@shared/testing'; +import { RegistrationPinRepo } from '.'; +import { registrationPinEntityFactory } from '../entity/testing'; + +describe(RegistrationPinRepo.name, () => { + let module: TestingModule; + let repo: RegistrationPinRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [RegistrationPinRepo], + }).compile(); + + repo = module.get(RegistrationPinRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('deleteRegistrationPinByEmail', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const userWithoutRegistrationPin = userFactory.buildWithId(); + const registrationPinForUser = registrationPinEntityFactory.buildWithId({ email: user.email }); + + await em.persistAndFlush(registrationPinForUser); + + return { + user, + userWithoutRegistrationPin, + }; + }; + + describe('when registrationPin exists', () => { + it('should delete registrationPins by email', async () => { + const { user } = await setup(); + + const result: number = await repo.deleteRegistrationPinByEmail(user.email); + + expect(result).toEqual(1); + }); + }); + + describe('when there is no registrationPin', () => { + it('should return empty array', async () => { + const { userWithoutRegistrationPin } = await setup(); + + const result: number = await repo.deleteRegistrationPinByEmail(userWithoutRegistrationPin.email); + expect(result).toEqual(0); + }); + }); + }); +}); diff --git a/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts new file mode 100644 index 00000000000..6ca68bc089d --- /dev/null +++ b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts @@ -0,0 +1,14 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { RegistrationPinEntity } from '../entity'; + +@Injectable() +export class RegistrationPinRepo { + constructor(private readonly em: EntityManager) {} + + async deleteRegistrationPinByEmail(email: string): Promise { + const promise: Promise = this.em.nativeDelete(RegistrationPinEntity, { email }); + + return promise; + } +} diff --git a/apps/server/src/modules/registration-pin/service/index.ts b/apps/server/src/modules/registration-pin/service/index.ts new file mode 100644 index 00000000000..c8eea287110 --- /dev/null +++ b/apps/server/src/modules/registration-pin/service/index.ts @@ -0,0 +1 @@ +export * from './registration-pin.service'; diff --git a/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts b/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts new file mode 100644 index 00000000000..b5c6a2f3296 --- /dev/null +++ b/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts @@ -0,0 +1,66 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities, userDoFactory } from '@shared/testing'; +import { RegistrationPinService } from '.'; +import { RegistrationPinRepo } from '../repo'; + +describe(RegistrationPinService.name, () => { + let module: TestingModule; + let service: RegistrationPinService; + let registrationPinRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + RegistrationPinService, + { + provide: RegistrationPinRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(RegistrationPinService); + registrationPinRepo = module.get(RegistrationPinRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('deleteRegistrationPinByEmail', () => { + describe('when deleting registrationPin', () => { + const setup = () => { + const user = userDoFactory.buildWithId(); + + registrationPinRepo.deleteRegistrationPinByEmail.mockResolvedValueOnce(1); + + return { + user, + }; + }; + + it('should call registrationPinRep', async () => { + const { user } = setup(); + + await service.deleteRegistrationPinByEmail(user.email); + + expect(registrationPinRepo.deleteRegistrationPinByEmail).toBeCalledWith(user.email); + }); + + it('should delete registrationPin by email', async () => { + const { user } = setup(); + + const result: number = await service.deleteRegistrationPinByEmail(user.email); + + expect(result).toEqual(1); + }); + }); + }); +}); diff --git a/apps/server/src/modules/registration-pin/service/registration-pin.service.ts b/apps/server/src/modules/registration-pin/service/registration-pin.service.ts new file mode 100644 index 00000000000..4681b08329c --- /dev/null +++ b/apps/server/src/modules/registration-pin/service/registration-pin.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { RegistrationPinRepo } from '../repo'; + +@Injectable() +export class RegistrationPinService { + constructor(private readonly registrationPinRepo: RegistrationPinRepo) {} + + async deleteRegistrationPinByEmail(email: string): Promise { + return this.registrationPinRepo.deleteRegistrationPinByEmail(email); + } +} diff --git a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts index dd8ae17667c..57d7c2da254 100644 --- a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts @@ -62,7 +62,7 @@ describe(RocketChatUserService.name, () => { }); }); - describe('deleteUserDataFromClasses', () => { + describe('delete RocketChatUser', () => { describe('when deleting rocketChatUser', () => { const setup = () => { const userId = new ObjectId().toHexString(); diff --git a/apps/server/src/modules/user/service/user.service.spec.ts b/apps/server/src/modules/user/service/user.service.spec.ts index f65d02c13a5..044d169864d 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -361,7 +361,6 @@ describe('UserService', () => { describe('when deleting by userId', () => { const setup = () => { const user1: User = userFactory.asStudent().buildWithId(); - userFactory.asStudent().buildWithId(); userRepo.findById.mockResolvedValue(user1); userRepo.deleteUser.mockResolvedValue(1); @@ -381,4 +380,33 @@ describe('UserService', () => { }); }); }); + + describe('getParentEmailsFromUser', () => { + const setup = () => { + const user: User = userFactory.asStudent().buildWithId(); + const parentEmail = ['test@test.eu']; + + userRepo.getParentEmailsFromUser.mockResolvedValue(parentEmail); + + return { + user, + parentEmail, + }; + }; + + it('should call userRepo.getParentEmailsFromUse', async () => { + const { user } = setup(); + + await service.getParentEmailsFromUser(user.id); + + expect(userRepo.getParentEmailsFromUser).toBeCalledWith(user.id); + }); + + it('should return array with parent emails', async () => { + const { user, parentEmail } = setup(); + + const result = await service.getParentEmailsFromUser(user.id); + expect(result).toEqual(parentEmail); + }); + }); }); diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index 6ef014f8696..8f6feca4750 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -120,4 +120,10 @@ export class UserService { return deletedUserNumber; } + + async getParentEmailsFromUser(userId: EntityId): Promise { + const parentEmails = this.userRepo.getParentEmailsFromUser(userId); + + return parentEmails; + } } diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 9dc33c55b78..a7ed0587f54 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -5,6 +5,7 @@ import { ShareToken } from '@modules/sharing/entity/share-token.entity'; import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { RegistrationPinEntity } from '@modules/registration-pin/entity'; import { Account } from './account.entity'; import { BoardNode, @@ -100,4 +101,5 @@ export const ALL_ENTITIES = [ UserLoginMigrationEntity, VideoConference, GroupEntity, + RegistrationPinEntity, ]; diff --git a/apps/server/src/shared/domain/entity/user-parents.entity.spec.ts b/apps/server/src/shared/domain/entity/user-parents.entity.spec.ts new file mode 100644 index 00000000000..d5fe53251f9 --- /dev/null +++ b/apps/server/src/shared/domain/entity/user-parents.entity.spec.ts @@ -0,0 +1,23 @@ +import { UserParentsEntity } from './user-parents.entity'; + +describe(UserParentsEntity.name, () => { + describe('constructor', () => { + describe('When a contructor is called', () => { + const setup = () => { + const entity = new UserParentsEntity({ firstName: 'firstName', lastName: 'lastName', email: 'test@test.eu' }); + + return { entity }; + }; + + it('should contain valid tspUid ', () => { + const { entity } = setup(); + + const userParentsEntity: UserParentsEntity = new UserParentsEntity(entity); + + expect(userParentsEntity.firstName).toEqual(entity.firstName); + expect(userParentsEntity.lastName).toEqual(entity.lastName); + expect(userParentsEntity.email).toEqual(entity.email); + }); + }); + }); +}); diff --git a/apps/server/src/shared/domain/entity/user-parents.entity.ts b/apps/server/src/shared/domain/entity/user-parents.entity.ts new file mode 100644 index 00000000000..a0709396880 --- /dev/null +++ b/apps/server/src/shared/domain/entity/user-parents.entity.ts @@ -0,0 +1,25 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +export interface UserParentsEntityProps { + firstName: string; + lastName: string; + email: string; +} + +@Embeddable() +export class UserParentsEntity { + @Property() + firstName: string; + + @Property() + lastName: string; + + @Property() + email: string; + + constructor(props: UserParentsEntityProps) { + this.firstName = props.firstName; + this.lastName = props.lastName; + this.email = props.email; + } +} diff --git a/apps/server/src/shared/domain/entity/user.entity.ts b/apps/server/src/shared/domain/entity/user.entity.ts index c9a982c3854..dd5c0ec66b3 100644 --- a/apps/server/src/shared/domain/entity/user.entity.ts +++ b/apps/server/src/shared/domain/entity/user.entity.ts @@ -1,8 +1,9 @@ -import { Collection, Entity, Index, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; +import { Collection, Embedded, Entity, Index, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; import { EntityWithSchool } from '../interface'; import { BaseEntityWithTimestamps } from './base.entity'; import { Role } from './role.entity'; import { SchoolEntity } from './school.entity'; +import { UserParentsEntity } from './user-parents.entity'; export enum LanguageType { DE = 'de', @@ -27,6 +28,7 @@ export interface UserProperties { outdatedSince?: Date; previousExternalId?: string; birthday?: Date; + parents?: UserParentsEntity[]; } @Entity({ tableName: 'users' }) @@ -100,6 +102,9 @@ export class User extends BaseEntityWithTimestamps implements EntityWithSchool { @Property({ nullable: true }) birthday?: Date; + @Embedded(() => UserParentsEntity, { array: true, nullable: true }) + parents?: UserParentsEntity[]; + constructor(props: UserProperties) { super(); this.firstName = props.firstName; @@ -117,6 +122,7 @@ export class User extends BaseEntityWithTimestamps implements EntityWithSchool { this.outdatedSince = props.outdatedSince; this.previousExternalId = props.previousExternalId; this.birthday = props.birthday; + this.parents = props.parents; } public resolvePermissions(): string[] { diff --git a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts index a923b8d128f..1ea116d995a 100644 --- a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts @@ -11,6 +11,7 @@ import { systemEntityFactory, userFactory, } from '@shared/testing'; +import { UserParentsEntityProps } from '@shared/domain/entity/user-parents.entity'; import { UserRepo } from './user.repo'; describe('user repo', () => { @@ -70,6 +71,7 @@ describe('user repo', () => { 'externalId', 'forcePasswordChange', 'importHash', + 'parents', 'preferences', 'language', 'deletedAt', @@ -160,6 +162,7 @@ describe('user repo', () => { 'externalId', 'forcePasswordChange', 'importHash', + 'parents', 'preferences', 'language', 'deletedAt', @@ -449,4 +452,36 @@ describe('user repo', () => { }); }); }); + + describe('getParentEmailsFromUser', () => { + const setup = async () => { + const parentOfUser: UserParentsEntityProps = { + firstName: 'firstName', + lastName: 'lastName', + email: 'test@test.eu', + }; + const user = userFactory.asStudent().buildWithId({ + parents: [parentOfUser], + }); + + const expectedParentEmail = [parentOfUser.email]; + + await em.persistAndFlush(user); + em.clear(); + + return { + user, + expectedParentEmail, + }; + }; + + describe('when searching user parent email', () => { + it('should return array witn parent email', async () => { + const { user, expectedParentEmail } = await setup(); + const result = await repo.getParentEmailsFromUser(user.id); + + expect(result).toEqual(expectedParentEmail); + }); + }); + }); }); diff --git a/apps/server/src/shared/repo/user/user.repo.ts b/apps/server/src/shared/repo/user/user.repo.ts index 44acafe6a80..2f693ab7124 100644 --- a/apps/server/src/shared/repo/user/user.repo.ts +++ b/apps/server/src/shared/repo/user/user.repo.ts @@ -170,6 +170,13 @@ export class UserRepo extends BaseRepo { return deletedUserNumber; } + async getParentEmailsFromUser(userId: EntityId): Promise { + const user = await this._em.findOneOrFail(User, { id: userId }); + const parentsEmails = user.parents?.map((parent) => parent.email) ?? []; + + return parentsEmails; + } + private async populateRoles(roles: Role[]): Promise { for (let i = 0; i < roles.length; i += 1) { const role = roles[i];