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 dbfc2e06d8d..1a4f3bcf425 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', + 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 063f3d46b48..34c34e302f5 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 @@ -9,14 +9,17 @@ import { LessonService } from '@modules/lesson/service'; import { PseudonymService } from '@modules/pseudonym'; import { TeamService } from '@modules/teams'; 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 { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; import { DeletionLogService } from '../services/deletion-log.service'; import { DeletionRequestService } from '../services'; import { DeletionRequestUc } from './deletion-request.uc'; -import { DeletionRequestLog, DeletionRequestProps } from './interface/interfaces'; import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; +import { DeletionRequestLog, DeletionRequestProps } from './interface'; describe(DeletionRequestUc.name, () => { let module: TestingModule; @@ -32,6 +35,8 @@ describe(DeletionRequestUc.name, () => { let pseudonymService: DeepMocked; let teamService: DeepMocked; let userService: DeepMocked; + let rocketChatUserService: DeepMocked; + let rocketChatService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -81,6 +86,14 @@ describe(DeletionRequestUc.name, () => { provide: UserService, useValue: createMock(), }, + { + provide: RocketChatUserService, + useValue: createMock(), + }, + { + provide: RocketChatService, + useValue: createMock(), + }, ], }).compile(); @@ -96,6 +109,8 @@ describe(DeletionRequestUc.name, () => { pseudonymService = module.get(PseudonymService); teamService = module.get(TeamService); userService = module.get(UserService); + rocketChatUserService = module.get(RocketChatUserService); + rocketChatService = module.get(RocketChatService); await setupEntities(); }); @@ -153,6 +168,9 @@ describe(DeletionRequestUc.name, () => { const setup = () => { jest.clearAllMocks(); const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); + const rocketChatUser: RocketChatUser = rocketChatUserFactory.build({ + userId: deletionRequestToExecute.targetRefId, + }); classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); @@ -163,9 +181,11 @@ describe(DeletionRequestUc.name, () => { pseudonymService.deleteByUserId.mockResolvedValueOnce(2); teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); userService.deleteUser.mockResolvedValueOnce(1); + rocketChatUserService.deleteByUserId.mockResolvedValueOnce(1); return { deletionRequestToExecute, + rocketChatUser, }; }; @@ -287,6 +307,38 @@ describe(DeletionRequestUc.name, () => { expect(userService.deleteUser).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); + it('should call rocketChatUserService.findByUserId to find rocketChatUser in rocketChatUser module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(rocketChatUserService.findByUserId).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call rocketChatUserService.deleteByUserId to delete rocketChatUser in rocketChatUser module', async () => { + const { deletionRequestToExecute, rocketChatUser } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + rocketChatUserService.findByUserId.mockResolvedValueOnce(rocketChatUser); + + await uc.executeDeletionRequests(); + + expect(rocketChatUserService.deleteByUserId).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call rocketChatService.deleteUser to delete rocketChatUser in rocketChat external module', async () => { + const { deletionRequestToExecute, rocketChatUser } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + rocketChatUserService.findByUserId.mockResolvedValueOnce(rocketChatUser); + + await uc.executeDeletionRequests(); + + expect(rocketChatService.deleteUser).toHaveBeenCalledWith(rocketChatUser.username); + }); + it('should call deletionLogService.createDeletionLog to create logs for deletionRequest', 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 d94a129310f..abea56fda96 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -8,6 +8,8 @@ import { LessonService } from '@modules/lesson/service'; import { CourseGroupService, CourseService } from '@modules/learnroom/service'; import { FilesService } from '@modules/files/service'; import { AccountService } from '@modules/account/services'; +import { RocketChatUserService } from '@modules/rocketchat-user'; +import { RocketChatService } from '@modules/rocketchat'; import { DeletionRequestService } from '../services/deletion-request.service'; import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; import { DeletionLogService } from '../services/deletion-log.service'; @@ -38,7 +40,9 @@ export class DeletionRequestUc { private readonly lessonService: LessonService, private readonly pseudonymService: PseudonymService, private readonly teamService: TeamService, - private readonly userService: UserService + private readonly userService: UserService, + private readonly rocketChatUserService: RocketChatUserService, + private readonly rocketChatService: RocketChatService ) {} async createDeletionRequest(deletionRequest: DeletionRequestProps): Promise { @@ -96,6 +100,7 @@ export class DeletionRequestUc { this.removeUsersPseudonyms(deletionRequest), this.removeUserFromTeams(deletionRequest), this.removeUser(deletionRequest), + this.removeUserFromRocketChat(deletionRequest), ]); await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); } catch (error) { @@ -206,4 +211,15 @@ export class DeletionRequestUc { const userDeleted: number = await this.userService.deleteUser(deletionRequest.targetRefId); await this.logDeletion(deletionRequest, DeletionDomainModel.USER, DeletionOperationModel.DELETE, 0, userDeleted); } + + private async removeUserFromRocketChat(deletionRequest: DeletionRequest): Promise { + const rocketChatUser = await this.rocketChatUserService.findByUserId(deletionRequest.targetRefId); + + const [, rocketChatUserDeleted] = await Promise.all([ + this.rocketChatService.deleteUser(rocketChatUser.username), + this.rocketChatUserService.deleteByUserId(rocketChatUser.userId), + ]); + + return rocketChatUserDeleted; + } } diff --git a/apps/server/src/modules/rocketchat-user/domain/index.ts b/apps/server/src/modules/rocketchat-user/domain/index.ts new file mode 100644 index 00000000000..0246dd0f0f9 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/domain/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.do'; diff --git a/apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.spec.ts b/apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.spec.ts new file mode 100644 index 00000000000..a1be448f80c --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.spec.ts @@ -0,0 +1,67 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RocketChatUser } from './rocket-chat-user.do'; +import { rocketChatUserFactory } from './testing/rocket-chat-user.factory'; + +describe(RocketChatUser.name, () => { + describe('constructor', () => { + describe('When constructor is called', () => { + it('should create a rocketChatUser by passing required properties', () => { + const domainObject: RocketChatUser = rocketChatUserFactory.build(); + + expect(domainObject instanceof RocketChatUser).toEqual(true); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const domainObject: RocketChatUser = rocketChatUserFactory.build(); + + return { domainObject }; + }; + + it('should set the id', () => { + const { domainObject } = setup(); + + const rocketChatUserObject: RocketChatUser = new RocketChatUser(domainObject); + + expect(rocketChatUserObject.id).toEqual(domainObject.id); + }); + }); + }); + + describe('getters', () => { + describe('When getters are used', () => { + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + userId: new ObjectId().toHexString(), + username: 'Test.User.shls', + rcId: 'JfMJXua6t29KYXdDc', + authToken: 'OL8e5YCZHy3agGnLS-gHAx1wU4ZCG8-DXU_WZnUxUu6', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const rocketChatUserDo = new RocketChatUser(props); + + return { props, rocketChatUserDo }; + }; + + it('getters should return proper values', () => { + const { props, rocketChatUserDo } = setup(); + + const gettersValues = { + id: rocketChatUserDo.id, + userId: rocketChatUserDo.userId, + username: rocketChatUserDo.username, + rcId: rocketChatUserDo.rcId, + authToken: rocketChatUserDo.authToken, + createdAt: rocketChatUserDo.createdAt, + updatedAt: rocketChatUserDo.updatedAt, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.ts b/apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.ts new file mode 100644 index 00000000000..8dfd830f3eb --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.ts @@ -0,0 +1,37 @@ +import { EntityId } from '@shared/domain/types'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; + +export interface RocketChatUserProps extends AuthorizableObject { + userId: EntityId; + username: string; + rcId: string; + authToken?: string; + createdAt?: Date; + updatedAt?: Date; +} + +export class RocketChatUser extends DomainObject { + get userId(): EntityId { + return this.props.userId; + } + + get username(): string { + return this.props.username; + } + + get rcId(): string { + return this.props.rcId; + } + + get authToken(): string | undefined { + return this.props.authToken; + } + + get createdAt(): Date | undefined { + return this.props.createdAt; + } + + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } +} diff --git a/apps/server/src/modules/rocketchat-user/domain/testing/index.ts b/apps/server/src/modules/rocketchat-user/domain/testing/index.ts new file mode 100644 index 00000000000..2ef434c0975 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/domain/testing/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.factory'; diff --git a/apps/server/src/modules/rocketchat-user/domain/testing/rocket-chat-user.factory.ts b/apps/server/src/modules/rocketchat-user/domain/testing/rocket-chat-user.factory.ts new file mode 100644 index 00000000000..3ad6432d1d5 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/domain/testing/rocket-chat-user.factory.ts @@ -0,0 +1,18 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { RocketChatUser, RocketChatUserProps } from '../rocket-chat-user.do'; + +export const rocketChatUserFactory = BaseFactory.define( + RocketChatUser, + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + userId: new ObjectId().toHexString(), + username: `username-${sequence}`, + rcId: `rcId-${sequence}`, + authToken: `aythToken-${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + }; + } +); diff --git a/apps/server/src/modules/rocketchat-user/entity/index.ts b/apps/server/src/modules/rocketchat-user/entity/index.ts new file mode 100644 index 00000000000..9528e8da500 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/entity/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.entity'; diff --git a/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.spec.ts b/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.spec.ts new file mode 100644 index 00000000000..f8d5318c5bf --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.spec.ts @@ -0,0 +1,61 @@ +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { RocketChatUserEntity } from '@src/modules/rocketchat-user/entity'; + +describe(RocketChatUserEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + userId: new ObjectId(), + username: 'Test.User.shls', + rcId: 'JfMJXua6t29KYXdDc', + authToken: 'OL8e5YCZHy3agGnLS-gHAx1wU4ZCG8-DXU_WZnUxUu6', + createdAt: new Date(), + updatedAt: new Date(), + }; + + 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 RocketChatUserEntity(); + expect(test).toThrow(); + }); + + it('should create a rocketChatUser by passing required properties', () => { + const { props } = setup(); + const entity: RocketChatUserEntity = new RocketChatUserEntity(props); + + expect(entity instanceof RocketChatUserEntity).toEqual(true); + }); + + it(`should return a valid object with fields values set from the provided complete props object`, () => { + const { props } = setup(); + const entity: RocketChatUserEntity = new RocketChatUserEntity(props); + + const entityProps = { + id: entity.id, + userId: entity.userId, + username: entity.username, + rcId: entity.rcId, + authToken: entity.authToken, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + expect(entityProps).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts b/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts new file mode 100644 index 00000000000..6df469e0ddb --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts @@ -0,0 +1,56 @@ +import { Entity, Index, Property, Unique } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain'; + +export interface RocketChatUserEntityProps { + id?: EntityId; + userId: ObjectId; + username: string; + rcId: string; + authToken?: string; + createdAt?: Date; + updatedAt?: Date; +} + +@Entity({ tableName: 'rocketchatuser' }) +export class RocketChatUserEntity extends BaseEntityWithTimestamps { + @Property() + @Unique() + username: string; + + @Property() + @Unique() + userId: ObjectId; + + @Property() + @Index() + rcId: string; + + @Property({ nullable: true }) + authToken?: string; + + constructor(props: RocketChatUserEntityProps) { + super(); + + if (props.id !== undefined) { + this.id = props.id; + } + + this.userId = props.userId; + this.username = props.username; + this.rcId = props.rcId; + + if (props.authToken !== undefined) { + this.authToken = props.authToken; + } + + if (props.createdAt !== undefined) { + this.createdAt = props.createdAt; + } + + if (props.updatedAt !== undefined) { + this.updatedAt = props.updatedAt; + } + } +} diff --git a/apps/server/src/modules/rocketchat-user/entity/testing/index.ts b/apps/server/src/modules/rocketchat-user/entity/testing/index.ts new file mode 100644 index 00000000000..f19ebd8c74a --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/entity/testing/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.entity.factory'; diff --git a/apps/server/src/modules/rocketchat-user/entity/testing/rocket-chat-user.entity.factory.ts b/apps/server/src/modules/rocketchat-user/entity/testing/rocket-chat-user.entity.factory.ts new file mode 100644 index 00000000000..302459a4eb6 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/entity/testing/rocket-chat-user.entity.factory.ts @@ -0,0 +1,20 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { RocketChatUserEntity, RocketChatUserEntityProps } from '../rocket-chat-user.entity'; + +class RocketChatUserFactory extends BaseFactory {} + +export const rocketChatUserEntityFactory = RocketChatUserFactory.define< + RocketChatUserEntity, + RocketChatUserEntityProps +>(RocketChatUserEntity, ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + userId: new ObjectId(), + username: `username-${sequence}`, + rcId: `rcId-${sequence}`, + authToken: `aythToken-${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/modules/rocketchat-user/index.ts b/apps/server/src/modules/rocketchat-user/index.ts new file mode 100644 index 00000000000..34ae0f25f87 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/index.ts @@ -0,0 +1,3 @@ +export * from './rocketchat-user.module'; +export * from './service'; +export * from './domain'; diff --git a/apps/server/src/modules/rocketchat-user/repo/index.ts b/apps/server/src/modules/rocketchat-user/repo/index.ts new file mode 100644 index 00000000000..b05b92fc380 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/repo/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.repo'; diff --git a/apps/server/src/modules/rocketchat-user/repo/mapper/index.ts b/apps/server/src/modules/rocketchat-user/repo/mapper/index.ts new file mode 100644 index 00000000000..7a33e93289e --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/repo/mapper/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.mapper'; diff --git a/apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.spec.ts b/apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.spec.ts new file mode 100644 index 00000000000..bd5a07abb5c --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.spec.ts @@ -0,0 +1,61 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RocketChatUser } from '../../domain/rocket-chat-user.do'; +import { rocketChatUserFactory } from '../../domain/testing/rocket-chat-user.factory'; +import { RocketChatUserEntity } from '../../entity'; +import { rocketChatUserEntityFactory } from '../../entity/testing/rocket-chat-user.entity.factory'; +import { RocketChatUserMapper } from './rocket-chat-user.mapper'; + +describe(RocketChatUserMapper.name, () => { + describe('mapToDO', () => { + describe('When entity is mapped for domainObject', () => { + it('should properly map the entity to the domain object', () => { + const entity = rocketChatUserEntityFactory.build(); + + const domainObject = RocketChatUserMapper.mapToDO(entity); + + const expectedDomainObject = new RocketChatUser({ + id: entity.id, + userId: entity.userId.toHexString(), + username: entity.username, + rcId: entity.rcId, + authToken: entity.authToken, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + + expect(domainObject).toEqual(expectedDomainObject); + }); + }); + }); + + describe('mapToEntity', () => { + describe('When domainObject is mapped for entity', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should properly map the domainObject to the entity', () => { + const domainObject = rocketChatUserFactory.build(); + + const entity = RocketChatUserMapper.mapToEntity(domainObject); + + const expectedEntity = new RocketChatUserEntity({ + id: domainObject.id, + userId: new ObjectId(domainObject.userId), + username: domainObject.username, + rcId: domainObject.rcId, + authToken: domainObject.authToken, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }); + + expect(entity).toEqual(expectedEntity); + }); + }); + }); +}); diff --git a/apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.ts b/apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.ts new file mode 100644 index 00000000000..3d45c9c34ac --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.ts @@ -0,0 +1,29 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RocketChatUserEntity } from '../../entity'; +import { RocketChatUser } from '../../domain/rocket-chat-user.do'; + +export class RocketChatUserMapper { + static mapToDO(entity: RocketChatUserEntity): RocketChatUser { + return new RocketChatUser({ + id: entity.id, + userId: entity.userId.toHexString(), + username: entity.username, + rcId: entity.rcId, + authToken: entity.authToken, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + } + + static mapToEntity(domainObject: RocketChatUser): RocketChatUserEntity { + return new RocketChatUserEntity({ + id: domainObject.id, + userId: new ObjectId(domainObject.userId), + username: domainObject.username, + rcId: domainObject.rcId, + authToken: domainObject.authToken, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }); + } +} diff --git a/apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.spec.ts b/apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.spec.ts new file mode 100644 index 00000000000..d58e5fc42d1 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.spec.ts @@ -0,0 +1,131 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { cleanupCollections } from '@shared/testing'; +import { RocketChatUserMapper } from './mapper'; +import { RocketChatUserEntity } from '../entity'; +import { RocketChatUserRepo } from './rocket-chat-user.repo'; +import { RocketChatUser } from '../domain'; +import { rocketChatUserEntityFactory } from '../entity/testing'; + +describe(RocketChatUserRepo.name, () => { + let module: TestingModule; + let repo: RocketChatUserRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MongoMemoryDatabaseModule.forRoot({ + entities: [RocketChatUserEntity], + }), + ], + providers: [RocketChatUserRepo, RocketChatUserMapper], + }).compile(); + + repo = module.get(RocketChatUserRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('defined', () => { + it('repo should be defined', () => { + expect(repo).toBeDefined(); + expect(typeof repo.findByUserId).toEqual('function'); + }); + + it('entity manager should be defined', () => { + expect(em).toBeDefined(); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(RocketChatUserEntity); + }); + }); + + describe('findByUserId', () => { + describe('when searching rocketChatUser by userId', () => { + const setup = async () => { + const userId = new ObjectId(); + const entity: RocketChatUserEntity = rocketChatUserEntityFactory.build({ userId }); + await em.persistAndFlush(entity); + em.clear(); + const expectedRocketChatUser = { + id: entity.id, + userId: entity.userId.toHexString(), + username: entity.username, + rcId: entity.rcId, + authToken: entity.authToken, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + return { + entity, + expectedRocketChatUser, + }; + }; + + it('should find the rocketChatUser', async () => { + const { entity, expectedRocketChatUser } = await setup(); + + const result: RocketChatUser = await repo.findByUserId(entity.userId.toHexString()); + + // Verify explicit fields. + expect(result).toEqual(expect.objectContaining(expectedRocketChatUser)); + }); + }); + }); + + describe('deleteByUserId', () => { + describe('when deleting rocketChatUser exists', () => { + const setup = async () => { + const entity: RocketChatUserEntity = rocketChatUserEntityFactory.build(); + const rocketChatUserId = entity.userId.toHexString(); + await em.persistAndFlush(entity); + em.clear(); + + return { rocketChatUserId }; + }; + + it('should delete the rocketChatUSer with userId', async () => { + const { rocketChatUserId } = await setup(); + + await repo.deleteByUserId(rocketChatUserId); + + expect(await em.findOne(RocketChatUserEntity, { userId: new ObjectId(rocketChatUserId) })).toBeNull(); + }); + + it('should return number equal 1', async () => { + const { rocketChatUserId } = await setup(); + + const result: number = await repo.deleteByUserId(rocketChatUserId); + + expect(result).toEqual(1); + }); + }); + + describe('when no rocketChatUser exists', () => { + const setup = () => { + const rocketChatUserId = new ObjectId().toHexString(); + + return { rocketChatUserId }; + }; + + it('should return false', async () => { + const { rocketChatUserId } = setup(); + + const result: number = await repo.deleteByUserId(rocketChatUserId); + + expect(result).toEqual(0); + }); + }); + }); +}); diff --git a/apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.ts b/apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.ts new file mode 100644 index 00000000000..741f297f804 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.ts @@ -0,0 +1,33 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { RocketChatUserEntity } from '../entity'; +import { RocketChatUser } from '../domain/rocket-chat-user.do'; +import { RocketChatUserMapper } from './mapper'; + +@Injectable() +export class RocketChatUserRepo { + constructor(private readonly em: EntityManager) {} + + get entityName() { + return RocketChatUserEntity; + } + + async findByUserId(userId: EntityId): Promise { + const entity: RocketChatUserEntity = await this.em.findOneOrFail(RocketChatUserEntity, { + userId: new ObjectId(userId), + }); + + const mapped: RocketChatUser = RocketChatUserMapper.mapToDO(entity); + + return mapped; + } + + async deleteByUserId(userId: EntityId): Promise { + const promise: Promise = this.em.nativeDelete(RocketChatUserEntity, { + userId: new ObjectId(userId), + }); + + return promise; + } +} diff --git a/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts b/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts new file mode 100644 index 00000000000..798b2276a4d --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { RocketChatUserRepo } from './repo'; +import { RocketChatUserService } from './service/rocket-chat-user.service'; +import { RocketChatService } from '../rocketchat/rocket-chat.service'; + +@Module({ + providers: [RocketChatUserService, RocketChatUserRepo], + exports: [RocketChatService], +}) +export class RocketChatUserModule {} diff --git a/apps/server/src/modules/rocketchat-user/service/index.ts b/apps/server/src/modules/rocketchat-user/service/index.ts new file mode 100644 index 00000000000..350217d4e38 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/service/index.ts @@ -0,0 +1 @@ +export * from './rocket-chat-user.service'; 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 new file mode 100644 index 00000000000..dd8ae17667c --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts @@ -0,0 +1,94 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { RocketChatUserService } from './rocket-chat-user.service'; +import { RocketChatUserRepo } from '../repo'; +import { rocketChatUserFactory } from '../domain/testing/rocket-chat-user.factory'; +import { RocketChatUser } from '../domain'; + +describe(RocketChatUserService.name, () => { + let module: TestingModule; + let service: RocketChatUserService; + let rocketChatUserRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + RocketChatUserService, + { + provide: RocketChatUserRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(RocketChatUserService); + rocketChatUserRepo = module.get(RocketChatUserRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('findByUserId', () => { + describe('when searching rocketChatUser', () => { + const setup = () => { + const userId: string = new ObjectId().toHexString(); + + const rocketChatUser: RocketChatUser = rocketChatUserFactory.build(); + + rocketChatUserRepo.findByUserId.mockResolvedValueOnce(rocketChatUser); + + return { + userId, + rocketChatUser, + }; + }; + + it('should return the rocketChatUser', async () => { + const { userId, rocketChatUser } = setup(); + + const result: RocketChatUser = await service.findByUserId(userId); + + expect(result).toEqual(rocketChatUser); + }); + }); + }); + + describe('deleteUserDataFromClasses', () => { + describe('when deleting rocketChatUser', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + + rocketChatUserRepo.deleteByUserId.mockResolvedValueOnce(1); + + return { + userId, + }; + }; + + it('should call rocketChatUserRepo', async () => { + const { userId } = setup(); + + await service.deleteByUserId(userId); + + expect(rocketChatUserRepo.deleteByUserId).toBeCalledWith(userId); + }); + + it('should delete rocketChatUser by userId', async () => { + const { userId } = setup(); + + const result: number = await service.deleteByUserId(userId); + + expect(result).toEqual(1); + }); + }); + }); +}); diff --git a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts new file mode 100644 index 00000000000..32a600c0f75 --- /dev/null +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { RocketChatUserRepo } from '../repo'; +import { RocketChatUser } from '../domain'; + +@Injectable() +export class RocketChatUserService { + constructor(private readonly rocketChatUserRepo: RocketChatUserRepo) {} + + public async findByUserId(userId: EntityId): Promise { + const user: RocketChatUser = await this.rocketChatUserRepo.findByUserId(userId); + + return user; + } + + public deleteByUserId(userId: EntityId): Promise { + return this.rocketChatUserRepo.deleteByUserId(userId); + } +}