Skip to content

Commit

Permalink
BC-5831-rewriting-regpin-deletion (#4570)
Browse files Browse the repository at this point in the history
* add registration-pin entity

* create module registrationPin

* changes in user repo and user service

* add registrationPindeletion ti vs in deletion module

* fix imports

* Update apps/server/src/shared/domain/entity/all-entities.ts

Co-authored-by: Sergej Hoffmann <[email protected]>

* Update apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts

Co-authored-by: Sergej Hoffmann <[email protected]>

* Update apps/server/src/modules/registration-pin/entity/registration-pin.entity.ts

Co-authored-by: Sergej Hoffmann <[email protected]>

* Update apps/server/src/shared/repo/user/user.repo.ts

Co-authored-by: Sergej Hoffmann <[email protected]>

* fix imports

* small fixes

* small fixes

* small fixes

* remove spaces

* add tests

---------

Co-authored-by: Sergej Hoffmann <[email protected]>
  • Loading branch information
WojciechGrancow and SevenWaysDP authored Nov 28, 2023
1 parent 537571d commit fd8dd82
Show file tree
Hide file tree
Showing 26 changed files with 483 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const enum DeletionDomainModel {
FILE = 'file',
LESSONS = 'lessons',
PSEUDONYMS = 'pseudonyms',
REGISTRATIONPIN = 'registrationPin',
ROCKETCHATUSER = 'rocketChatUser',
TEAMS = 'teams',
USER = 'user',
Expand Down
37 changes: 36 additions & 1 deletion apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -37,6 +38,7 @@ describe(DeletionRequestUc.name, () => {
let userService: DeepMocked<UserService>;
let rocketChatUserService: DeepMocked<RocketChatUserService>;
let rocketChatService: DeepMocked<RocketChatService>;
let registrationPinService: DeepMocked<RegistrationPinService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand Down Expand Up @@ -94,6 +96,10 @@ describe(DeletionRequestUc.name, () => {
provide: RocketChatService,
useValue: createMock<RocketChatService>(),
},
{
provide: RegistrationPinService,
useValue: createMock<RegistrationPinService>(),
},
],
}).compile();

Expand All @@ -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();
});

Expand Down Expand Up @@ -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 = '[email protected]';

registrationPinService.deleteRegistrationPinByEmail.mockResolvedValueOnce(2);
classService.deleteUserDataFromClasses.mockResolvedValueOnce(1);
courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2);
courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2);
Expand All @@ -186,6 +196,8 @@ describe(DeletionRequestUc.name, () => {
return {
deletionRequestToExecute,
rocketChatUser,
user,
parentEmail,
};
};

Expand Down Expand Up @@ -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();

Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/modules/deletion/uc/deletion-request.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<DeletionRequestCreateAnswer> {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/registration-pin/entity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './registration-pin.entity';
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './registration-pin.entity.factory';
Original file line number Diff line number Diff line change
@@ -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, RegistrationPinEntityProps>(
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(),
};
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './factory';
2 changes: 2 additions & 0 deletions apps/server/src/modules/registration-pin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './registration-pin.module';
export { RegistrationPinService } from './service';
Original file line number Diff line number Diff line change
@@ -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 {}
1 change: 1 addition & 0 deletions apps/server/src/modules/registration-pin/repo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './registration-pin.repo';
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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<number> {
const promise: Promise<number> = this.em.nativeDelete(RegistrationPinEntity, { email });

return promise;
}
}
1 change: 1 addition & 0 deletions apps/server/src/modules/registration-pin/service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './registration-pin.service';
Loading

0 comments on commit fd8dd82

Please sign in to comment.