From cbf3940e830e7565cc0a1dcc09e0bb6771a80e64 Mon Sep 17 00:00:00 2001 From: WahlMartin <132356096+WahlMartin@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:28:47 +0100 Subject: [PATCH 01/23] BC-4887 add sort for users to be migrated and clear cache (#4524) --- apps/server/src/modules/account/repo/account.repo.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/account/repo/account.repo.ts b/apps/server/src/modules/account/repo/account.repo.ts index fb68f0a759b..d1872d1ca67 100644 --- a/apps/server/src/modules/account/repo/account.repo.ts +++ b/apps/server/src/modules/account/repo/account.repo.ts @@ -1,7 +1,7 @@ import { AnyEntity, EntityName, Primary } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain'; +import { EntityId, SortOrder } from '@shared/domain'; import { Account } from '@shared/domain/entity/account.entity'; import { BaseRepo } from '@shared/repo/base.repo'; @@ -71,7 +71,9 @@ export class AccountRepo extends BaseRepo { * @deprecated For migration purpose only */ async findMany(offset = 0, limit = 100): Promise { - return this._em.find(this.entityName, {}, { offset, limit }); + const result = await this._em.find(this.entityName, {}, { offset, limit, orderBy: { id: SortOrder.asc } }); + this._em.clear(); + return result; } private async searchByUsername( From 01065dca0619375fd460fb4fead22448f32c4330 Mon Sep 17 00:00:00 2001 From: WojciechGrancow <116577704+WojciechGrancow@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:16:49 +0100 Subject: [PATCH 02/23] BC-5521 - Implement of KNL Control Module (#4486) * first commit * add some tests * add test cases and services * add usecases and test cases * fix importing * add type in uc * fix import * fix most of issue form review * add some test,additional status, limit ... * changing limit parameter and changing in useCases * Update apps/server/src/modules/deletion/uc/deletion-request.uc.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * Update apps/server/src/modules/deletion/uc/deletion-request.uc.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * Update apps/server/src/modules/deletion/services/deletion-request.service.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * Update apps/server/src/modules/deletion/deletion.module.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * fixes after review * small fixes --------- Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> --- .../src/modules/deletion/deletion.module.ts | 11 + .../deletion/domain/deletion-log.do.spec.ts | 70 +++ .../deletion/domain/deletion-log.do.ts | 44 ++ .../domain/deletion-request.do.spec.ts | 69 +++ .../deletion/domain/deletion-request.do.ts | 39 ++ .../testing/factory/deletion-log.factory.ts | 18 + .../factory/deletion-request.factory.ts | 28 ++ .../types/deletion-domain-model.enum.ts | 11 + .../types/deletion-operation-model.enum.ts | 4 + .../types/deletion-status-model.enum.ts | 5 + .../entity/deletion-log.entity.spec.ts | 60 +++ .../deletion/entity/deletion-log.entity.ts | 67 +++ .../entity/deletion-request.entity.spec.ts | 85 ++++ .../entity/deletion-request.entity.ts | 60 +++ .../src/modules/deletion/entity/index.ts | 2 + .../factory/deletion-log.entity.factory.ts | 21 + .../deletion-request.entity.factory.ts | 20 + apps/server/src/modules/deletion/index.ts | 2 + .../deletion/repo/deletion-log.repo.spec.ts | 190 ++++++++ .../deletion/repo/deletion-log.repo.ts | 41 ++ .../deletion/repo/deletion-request-scope.ts | 17 + .../repo/deletion-request.repo.spec.ts | 342 +++++++++++++ .../deletion/repo/deletion-request.repo.ts | 86 ++++ .../server/src/modules/deletion/repo/index.ts | 2 + .../repo/mapper/deletion-log.mapper.spec.ts | 162 +++++++ .../repo/mapper/deletion-log.mapper.ts | 39 ++ .../mapper/deletion-request.mapper.spec.ts | 71 +++ .../repo/mapper/deletion-request.mapper.ts | 28 ++ .../src/modules/deletion/repo/mapper/index.ts | 2 + .../services/deletion-log.service.spec.ts | 110 +++++ .../deletion/services/deletion-log.service.ts | 37 ++ .../services/deletion-request.service.spec.ts | 200 ++++++++ .../services/deletion-request.service.ts | 61 +++ .../src/modules/deletion/services/index.ts | 1 + .../deletion-log-statistic.builder.spec.ts | 22 + .../builder/deletion-log-statistic.builder.ts | 10 + .../deletion-request-log.builder.spec.ts | 28 ++ .../builder/deletion-request-log.builder.ts | 13 + .../deletion-target-ref.builder.spec.ts | 20 + .../uc/builder/deletion-target-ref.builder.ts | 11 + .../deletion/uc/deletion-request.uc.spec.ts | 459 ++++++++++++++++++ .../deletion/uc/deletion-request.uc.ts | 209 ++++++++ .../modules/deletion/uc/interface/index.ts | 1 + .../deletion/uc/interface/interfaces.ts | 29 ++ .../src/modules/learnroom/service/index.ts | 1 + 45 files changed, 2808 insertions(+) create mode 100644 apps/server/src/modules/deletion/deletion.module.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-log.do.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-request.do.ts create mode 100644 apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts create mode 100644 apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts create mode 100644 apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts create mode 100644 apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts create mode 100644 apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts create mode 100644 apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts create mode 100644 apps/server/src/modules/deletion/entity/deletion-log.entity.ts create mode 100644 apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts create mode 100644 apps/server/src/modules/deletion/entity/deletion-request.entity.ts create mode 100644 apps/server/src/modules/deletion/entity/index.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts create mode 100644 apps/server/src/modules/deletion/index.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-log.repo.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-request-scope.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-request.repo.ts create mode 100644 apps/server/src/modules/deletion/repo/index.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/index.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-log.service.spec.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-log.service.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-request.service.spec.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-request.service.ts create mode 100644 apps/server/src/modules/deletion/services/index.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/deletion-request.uc.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/index.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/interfaces.ts diff --git a/apps/server/src/modules/deletion/deletion.module.ts b/apps/server/src/modules/deletion/deletion.module.ts new file mode 100644 index 00000000000..440a9418d70 --- /dev/null +++ b/apps/server/src/modules/deletion/deletion.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; +import { DeletionRequestService } from './services/deletion-request.service'; +import { DeletionRequestRepo } from './repo/deletion-request.repo'; + +@Module({ + imports: [LoggerModule], + providers: [DeletionRequestService, DeletionRequestRepo], + exports: [DeletionRequestService], +}) +export class DeletionModule {} diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts new file mode 100644 index 00000000000..9117ded29c5 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts @@ -0,0 +1,70 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { deletionLogFactory } from './testing/factory/deletion-log.factory'; +import { DeletionLog } from './deletion-log.do'; +import { DeletionOperationModel } from './types/deletion-operation-model.enum'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; + +describe(DeletionLog.name, () => { + describe('constructor', () => { + describe('When constructor is called', () => { + it('should create a deletionRequest by passing required properties', () => { + const domainObject: DeletionLog = deletionLogFactory.build(); + + expect(domainObject instanceof DeletionLog).toEqual(true); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const domainObject: DeletionLog = deletionLogFactory.buildWithId(); + + return { domainObject }; + }; + + it('should set the id', () => { + const { domainObject } = setup(); + + const deletionLogDomainObject: DeletionLog = new DeletionLog(domainObject); + + expect(deletionLogDomainObject.id).toEqual(domainObject.id); + }); + }); + }); + + describe('getters', () => { + describe('When getters are used', () => { + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCount: 0, + deletedCount: 1, + deletionRequestId: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: new Date(), + }; + + const deletionLogDo = new DeletionLog(props); + + return { props, deletionLogDo }; + }; + it('getters should return proper values', () => { + const { props, deletionLogDo } = setup(); + + const gettersValues = { + id: deletionLogDo.id, + domain: deletionLogDo.domain, + operation: deletionLogDo.operation, + modifiedCount: deletionLogDo.modifiedCount, + deletedCount: deletionLogDo.deletedCount, + deletionRequestId: deletionLogDo.deletionRequestId, + createdAt: deletionLogDo.createdAt, + updatedAt: deletionLogDo.updatedAt, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.ts new file mode 100644 index 00000000000..73e62b46055 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.ts @@ -0,0 +1,44 @@ +import { EntityId } from '@shared/domain/types'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { DeletionOperationModel } from './types/deletion-operation-model.enum'; + +export interface DeletionLogProps extends AuthorizableObject { + createdAt?: Date; + updatedAt?: Date; + domain: DeletionDomainModel; + operation?: DeletionOperationModel; + modifiedCount?: number; + deletedCount?: number; + deletionRequestId?: EntityId; +} + +export class DeletionLog extends DomainObject { + get createdAt(): Date | undefined { + return this.props.createdAt; + } + + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } + + get domain(): DeletionDomainModel { + return this.props.domain; + } + + get operation(): DeletionOperationModel | undefined { + return this.props.operation; + } + + get modifiedCount(): number | undefined { + return this.props.modifiedCount; + } + + get deletedCount(): number | undefined { + return this.props.deletedCount; + } + + get deletionRequestId(): EntityId | undefined { + return this.props.deletionRequestId; + } +} diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts new file mode 100644 index 00000000000..3c0eb608c87 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts @@ -0,0 +1,69 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequest } from './deletion-request.do'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { deletionRequestFactory } from './testing/factory/deletion-request.factory'; +import { DeletionStatusModel } from './types/deletion-status-model.enum'; + +describe(DeletionRequest.name, () => { + describe('constructor', () => { + describe('When constructor is called', () => { + it('should create a deletionRequest by passing required properties', () => { + const domainObject: DeletionRequest = deletionRequestFactory.build(); + + expect(domainObject instanceof DeletionRequest).toEqual(true); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const domainObject: DeletionRequest = deletionRequestFactory.buildWithId(); + + return { domainObject }; + }; + + it('should set the id', () => { + const { domainObject } = setup(); + + const deletionRequestDomainObject: DeletionRequest = new DeletionRequest(domainObject); + + expect(deletionRequestDomainObject.id).toEqual(domainObject.id); + }); + }); + }); + + describe('getters', () => { + describe('When getters are used', () => { + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + targetRefDomain: DeletionDomainModel.USER, + deleteAfter: new Date(), + targetRefId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const deletionRequestDo = new DeletionRequest(props); + + return { props, deletionRequestDo }; + }; + + it('getters should return proper values', () => { + const { props, deletionRequestDo } = setup(); + + const gettersValues = { + id: deletionRequestDo.id, + targetRefDomain: deletionRequestDo.targetRefDomain, + deleteAfter: deletionRequestDo.deleteAfter, + targetRefId: deletionRequestDo.targetRefId, + status: deletionRequestDo.status, + createdAt: deletionRequestDo.createdAt, + updatedAt: deletionRequestDo.updatedAt, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.ts new file mode 100644 index 00000000000..e1a8b289ef0 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -0,0 +1,39 @@ +import { EntityId } from '@shared/domain/types'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { DeletionStatusModel } from './types/deletion-status-model.enum'; + +export interface DeletionRequestProps extends AuthorizableObject { + createdAt?: Date; + updatedAt?: Date; + targetRefDomain: DeletionDomainModel; + deleteAfter: Date; + targetRefId: EntityId; + status: DeletionStatusModel; +} + +export class DeletionRequest extends DomainObject { + get createdAt(): Date | undefined { + return this.props.createdAt; + } + + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } + + get targetRefDomain(): DeletionDomainModel { + return this.props.targetRefDomain; + } + + get deleteAfter(): Date { + return this.props.deleteAfter; + } + + get targetRefId(): EntityId { + return this.props.targetRefId; + } + + get status(): DeletionStatusModel { + return this.props.status; + } +} diff --git a/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts new file mode 100644 index 00000000000..d83b2f44c8a --- /dev/null +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts @@ -0,0 +1,18 @@ +import { DoBaseFactory } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLog, DeletionLogProps } from '../../deletion-log.do'; +import { DeletionOperationModel } from '../../types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '../../types/deletion-domain-model.enum'; + +export const deletionLogFactory = DoBaseFactory.define(DeletionLog, () => { + return { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCount: 0, + deletedCount: 1, + deletionRequestId: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts b/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts new file mode 100644 index 00000000000..9f87bbc1cbf --- /dev/null +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts @@ -0,0 +1,28 @@ +import { DoBaseFactory } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeepPartial } from 'fishery'; +import { DeletionRequest, DeletionRequestProps } from '../../deletion-request.do'; +import { DeletionDomainModel } from '../../types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../../types/deletion-status-model.enum'; + +class DeletionRequestFactory extends DoBaseFactory { + withUserIds(id: string): this { + const params: DeepPartial = { + targetRefId: id, + }; + + return this.params(params); + } +} + +export const deletionRequestFactory = DeletionRequestFactory.define(DeletionRequest, () => { + return { + id: new ObjectId().toHexString(), + targetRefDomain: DeletionDomainModel.USER, + deleteAfter: new Date(), + targetRefId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), + }; +}); 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 new file mode 100644 index 00000000000..dbfc2e06d8d --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts @@ -0,0 +1,11 @@ +export const enum DeletionDomainModel { + ACCOUNT = 'account', + CLASS = 'class', + COURSEGROUP = 'courseGroup', + COURSE = 'course', + FILE = 'file', + LESSONS = 'lessons', + PSEUDONYMS = 'pseudonyms', + TEAMS = 'teams', + USER = 'user', +} diff --git a/apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts b/apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts new file mode 100644 index 00000000000..675189e634b --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts @@ -0,0 +1,4 @@ +export const enum DeletionOperationModel { + DELETE = 'delete', + UPDATE = 'update', +} diff --git a/apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts b/apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts new file mode 100644 index 00000000000..5681d1be214 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts @@ -0,0 +1,5 @@ +export const enum DeletionStatusModel { + FAILED = 'failed', + REGISTERED = 'registered', + SUCCESS = 'success', +} diff --git a/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts new file mode 100644 index 00000000000..4f9f098cbb3 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts @@ -0,0 +1,60 @@ +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLogEntity } from './deletion-log.entity'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; + +describe(DeletionLogEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + describe('constructor', () => { + describe('When constructor is called', () => { + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCount: 0, + deletedCount: 1, + deletionRequestId: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + }; + + return { props }; + }; + it('should throw an error by empty constructor', () => { + // @ts-expect-error: Test case + const test = () => new DeletionLogEntity(); + expect(test).toThrow(); + }); + + it('should create a deletionLog by passing required properties', () => { + const { props } = setup(); + const entity: DeletionLogEntity = new DeletionLogEntity(props); + + expect(entity instanceof DeletionLogEntity).toEqual(true); + }); + + it(`should return a valid object with fields values set from the provided complete props object`, () => { + const { props } = setup(); + const entity: DeletionLogEntity = new DeletionLogEntity(props); + + const entityProps = { + id: entity.id, + domain: entity.domain, + operation: entity.operation, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, + deletionRequestId: entity.deletionRequestId, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + expect(entityProps).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts new file mode 100644 index 00000000000..8a9d2bab025 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts @@ -0,0 +1,67 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; + +export interface DeletionLogEntityProps { + id?: EntityId; + domain: DeletionDomainModel; + operation?: DeletionOperationModel; + modifiedCount?: number; + deletedCount?: number; + deletionRequestId?: ObjectId; + createdAt?: Date; + updatedAt?: Date; +} + +@Entity({ tableName: 'deletionlogs' }) +export class DeletionLogEntity extends BaseEntityWithTimestamps { + @Property() + domain: DeletionDomainModel; + + @Property({ nullable: true }) + operation?: DeletionOperationModel; + + @Property({ nullable: true }) + modifiedCount?: number; + + @Property({ nullable: true }) + deletedCount?: number; + + @Property({ nullable: true }) + deletionRequestId?: ObjectId; + + constructor(props: DeletionLogEntityProps) { + super(); + if (props.id !== undefined) { + this.id = props.id; + } + + this.domain = props.domain; + + if (props.operation !== undefined) { + this.operation = props.operation; + } + + if (props.modifiedCount !== undefined) { + this.modifiedCount = props.modifiedCount; + } + + if (props.deletedCount !== undefined) { + this.deletedCount = props.deletedCount; + } + + if (props.deletionRequestId !== undefined) { + this.deletionRequestId = props.deletionRequestId; + } + + if (props.createdAt !== undefined) { + this.createdAt = props.createdAt; + } + + if (props.updatedAt !== undefined) { + this.updatedAt = props.updatedAt; + } + } +} diff --git a/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts new file mode 100644 index 00000000000..6a0e416d580 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts @@ -0,0 +1,85 @@ +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequestEntity } from '@src/modules/deletion/entity/deletion-request.entity'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +describe(DeletionRequestEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + targetRefDomain: DeletionDomainModel.USER, + deleteAfter: new Date(), + targetRefId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + 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 DeletionRequestEntity(); + expect(test).toThrow(); + }); + + it('should create a deletionRequest by passing required properties', () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + expect(entity instanceof DeletionRequestEntity).toEqual(true); + }); + + it(`should return a valid object with fields values set from the provided complete props object`, () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + const entityProps = { + id: entity.id, + targetRefDomain: entity.targetRefDomain, + deleteAfter: entity.deleteAfter, + targetRefId: entity.targetRefId, + status: entity.status, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + expect(entityProps).toEqual(props); + }); + }); + }); + + describe('executed', () => { + it('should update status with value success', () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + entity.executed(); + + expect(entity.status).toEqual(DeletionStatusModel.SUCCESS); + }); + }); + + describe('failed', () => { + it('should update status with value failed', () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + entity.failed(); + + expect(entity.status).toEqual(DeletionStatusModel.FAILED); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts new file mode 100644 index 00000000000..150fed4d91e --- /dev/null +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts @@ -0,0 +1,60 @@ +import { Entity, Index, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +export interface DeletionRequestEntityProps { + id?: EntityId; + targetRefDomain: DeletionDomainModel; + deleteAfter: Date; + targetRefId: EntityId; + status: DeletionStatusModel; + createdAt?: Date; + updatedAt?: Date; +} + +@Entity({ tableName: 'deletionrequests' }) +@Index({ properties: ['targetRefId', 'targetRefDomain'] }) +export class DeletionRequestEntity extends BaseEntityWithTimestamps { + @Property() + deleteAfter: Date; + + @Property() + targetRefId: EntityId; + + @Property() + targetRefDomain: DeletionDomainModel; + + @Property() + @Index() + status: DeletionStatusModel; + + constructor(props: DeletionRequestEntityProps) { + super(); + if (props.id !== undefined) { + this.id = props.id; + } + + this.targetRefDomain = props.targetRefDomain; + this.deleteAfter = props.deleteAfter; + this.targetRefId = props.targetRefId; + this.status = props.status; + + if (props.createdAt !== undefined) { + this.createdAt = props.createdAt; + } + + if (props.updatedAt !== undefined) { + this.updatedAt = props.updatedAt; + } + } + + public executed(): void { + this.status = DeletionStatusModel.SUCCESS; + } + + public failed(): void { + this.status = DeletionStatusModel.FAILED; + } +} diff --git a/apps/server/src/modules/deletion/entity/index.ts b/apps/server/src/modules/deletion/entity/index.ts new file mode 100644 index 00000000000..7e3e31dcd19 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-request.entity'; +export * from './deletion-log.entity'; diff --git a/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts new file mode 100644 index 00000000000..897fba6820a --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { DeletionLogEntity, DeletionLogEntityProps } from '../../deletion-log.entity'; +import { DeletionOperationModel } from '../../../domain/types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '../../../domain/types/deletion-domain-model.enum'; + +export const deletionLogEntityFactory = BaseFactory.define( + DeletionLogEntity, + () => { + return { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCount: 0, + deletedCount: 1, + deletionRequestId: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + }; + } +); diff --git a/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts new file mode 100644 index 00000000000..3ccba779e3e --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts @@ -0,0 +1,20 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { DeletionStatusModel } from '../../../domain/types/deletion-status-model.enum'; +import { DeletionRequestEntity, DeletionRequestEntityProps } from '../../deletion-request.entity'; +import { DeletionDomainModel } from '../../../domain/types/deletion-domain-model.enum'; + +export const deletionRequestEntityFactory = BaseFactory.define( + DeletionRequestEntity, + () => { + return { + id: new ObjectId().toHexString(), + targetRefDomain: DeletionDomainModel.USER, + deleteAfter: new Date(), + targetRefId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), + }; + } +); diff --git a/apps/server/src/modules/deletion/index.ts b/apps/server/src/modules/deletion/index.ts new file mode 100644 index 00000000000..bd89c1e8d84 --- /dev/null +++ b/apps/server/src/modules/deletion/index.ts @@ -0,0 +1,2 @@ +export * from './deletion.module'; +export * from './services'; diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts new file mode 100644 index 00000000000..5bc151c3541 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts @@ -0,0 +1,190 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing/testing-module'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections } from '@shared/testing'; +import { DeletionLogMapper } from './mapper'; +import { DeletionLogEntity } from '../entity'; +import { DeletionLogRepo } from './deletion-log.repo'; +import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { deletionLogEntityFactory } from '../entity/testing/factory/deletion-log.entity.factory'; + +describe(DeletionLogRepo.name, () => { + let module: TestingModule; + let repo: DeletionLogRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MongoMemoryDatabaseModule.forRoot({ + entities: [DeletionLogEntity], + }), + ], + providers: [DeletionLogRepo, DeletionLogMapper], + }).compile(); + + repo = module.get(DeletionLogRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('defined', () => { + it('repo should be defined', () => { + expect(repo).toBeDefined(); + }); + + it('entity manager should be defined', () => { + expect(em).toBeDefined(); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(DeletionLogEntity); + }); + }); + + describe('create deletionLog', () => { + describe('when deletionLog is new', () => { + const setup = () => { + const domainObject: DeletionLog = deletionLogFactory.build(); + const deletionLogId = domainObject.id; + + const expectedDomainObject = { + id: domainObject.id, + domain: domainObject.domain, + operation: domainObject.operation, + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, + deletionRequestId: domainObject.deletionRequestId, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }; + + return { domainObject, deletionLogId, expectedDomainObject }; + }; + it('should create a new deletionLog', async () => { + const { domainObject, deletionLogId, expectedDomainObject } = setup(); + await repo.create(domainObject); + + const result = await repo.findById(deletionLogId); + + expect(result).toEqual(expect.objectContaining(expectedDomainObject)); + }); + }); + }); + + describe('findById', () => { + describe('when searching by Id', () => { + const setup = async () => { + // Test deletionLog entity + const entity: DeletionLogEntity = deletionLogEntityFactory.build(); + await em.persistAndFlush(entity); + + const expectedDeletionLog = { + id: entity.id, + domain: entity.domain, + operation: entity.operation, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, + deletionRequestId: entity.deletionRequestId?.toHexString(), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + return { + entity, + expectedDeletionLog, + }; + }; + + it('should find the deletionRequest', async () => { + const { entity, expectedDeletionLog } = await setup(); + + const result: DeletionLog = await repo.findById(entity.id); + + // Verify explicit fields. + expect(result).toEqual(expect.objectContaining(expectedDeletionLog)); + }); + }); + }); + + describe('findAllByDeletionRequestId', () => { + describe('when there is no deletionLog for deletionRequestId', () => { + it('should return empty array', async () => { + const deletionRequestId = new ObjectId().toHexString(); + const result = await repo.findAllByDeletionRequestId(deletionRequestId); + + expect(result).toEqual([]); + }); + }); + + describe('when searching by deletionRequestId', () => { + const setup = async () => { + const deletionRequest1Id = new ObjectId(); + const deletionRequest2Id = new ObjectId(); + const deletionLogEntity1: DeletionLogEntity = deletionLogEntityFactory.build({ + deletionRequestId: deletionRequest1Id, + }); + const deletionLogEntity2: DeletionLogEntity = deletionLogEntityFactory.build({ + deletionRequestId: deletionRequest1Id, + }); + const deletionLogEntity3: DeletionLogEntity = deletionLogEntityFactory.build({ + deletionRequestId: deletionRequest2Id, + }); + + await em.persistAndFlush([deletionLogEntity1, deletionLogEntity2, deletionLogEntity3]); + em.clear(); + + const expectedArray = [ + { + id: deletionLogEntity1.id, + domain: deletionLogEntity1.domain, + operation: deletionLogEntity1.operation, + deletionRequestId: deletionLogEntity1.deletionRequestId?.toHexString(), + modifiedCount: deletionLogEntity1.modifiedCount, + deletedCount: deletionLogEntity1.deletedCount, + createdAt: deletionLogEntity1.createdAt, + updatedAt: deletionLogEntity1.updatedAt, + }, + { + id: deletionLogEntity2.id, + domain: deletionLogEntity2.domain, + operation: deletionLogEntity2.operation, + deletionRequestId: deletionLogEntity2.deletionRequestId?.toHexString(), + modifiedCount: deletionLogEntity2.modifiedCount, + deletedCount: deletionLogEntity2.deletedCount, + createdAt: deletionLogEntity2.createdAt, + updatedAt: deletionLogEntity2.updatedAt, + }, + ]; + + return { deletionLogEntity3, deletionRequest1Id, expectedArray }; + }; + + it('should find deletionRequests with deleteAfter smaller then today', async () => { + const { deletionLogEntity3, deletionRequest1Id, expectedArray } = await setup(); + + const results = await repo.findAllByDeletionRequestId(deletionRequest1Id.toHexString()); + + expect(results.length).toEqual(2); + + // Verify explicit fields. + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining(expectedArray[0]), expect.objectContaining(expectedArray[1])]) + ); + + const result: DeletionLog = await repo.findById(deletionLogEntity3.id); + + expect(result.id).toEqual(deletionLogEntity3.id); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts new file mode 100644 index 00000000000..d71032eb124 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts @@ -0,0 +1,41 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { DeletionLogEntity } from '../entity/deletion-log.entity'; +import { DeletionLogMapper } from './mapper/deletion-log.mapper'; + +@Injectable() +export class DeletionLogRepo { + constructor(private readonly em: EntityManager) {} + + get entityName() { + return DeletionLogEntity; + } + + async findById(deletionLogId: EntityId): Promise { + const deletionLog: DeletionLogEntity = await this.em.findOneOrFail(DeletionLogEntity, { + id: deletionLogId, + }); + + const mapped: DeletionLog = DeletionLogMapper.mapToDO(deletionLog); + + return mapped; + } + + async findAllByDeletionRequestId(deletionRequestId: EntityId): Promise { + const deletionLogEntities: DeletionLogEntity[] = await this.em.find(DeletionLogEntity, { + deletionRequestId: new ObjectId(deletionRequestId), + }); + + const mapped: DeletionLog[] = DeletionLogMapper.mapToDOs(deletionLogEntities); + + return mapped; + } + + async create(deletionLog: DeletionLog): Promise { + const deletionLogEntity: DeletionLogEntity = DeletionLogMapper.mapToEntity(deletionLog); + this.em.persist(deletionLogEntity); + await this.em.flush(); + } +} diff --git a/apps/server/src/modules/deletion/repo/deletion-request-scope.ts b/apps/server/src/modules/deletion/repo/deletion-request-scope.ts new file mode 100644 index 00000000000..202bc09a887 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-request-scope.ts @@ -0,0 +1,17 @@ +import { Scope } from '@shared/repo'; +import { DeletionRequestEntity } from '../entity'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +export class DeletionRequestScope extends Scope { + byDeleteAfter(currentDate: Date): DeletionRequestScope { + this.addQuery({ deleteAfter: { $lt: currentDate } }); + + return this; + } + + byStatus(): DeletionRequestScope { + this.addQuery({ status: [DeletionStatusModel.REGISTERED, DeletionStatusModel.FAILED] }); + + return this; + } +} diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts new file mode 100644 index 00000000000..579dc0ec403 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts @@ -0,0 +1,342 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing/testing-module'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections } from '@shared/testing'; +import { DeletionRequestMapper } from './mapper'; +import { DeletionRequestRepo } from './deletion-request.repo'; +import { DeletionRequestEntity } from '../entity'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { deletionRequestEntityFactory } from '../entity/testing/factory/deletion-request.entity.factory'; +import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +describe(DeletionRequestRepo.name, () => { + let module: TestingModule; + let repo: DeletionRequestRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MongoMemoryDatabaseModule.forRoot({ + entities: [DeletionRequestEntity], + }), + ], + providers: [DeletionRequestRepo, DeletionRequestMapper], + }).compile(); + + repo = module.get(DeletionRequestRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('defined', () => { + it('repo should be defined', () => { + expect(repo).toBeDefined(); + }); + + it('entity manager should be defined', () => { + expect(em).toBeDefined(); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(DeletionRequestEntity); + }); + }); + + describe('create deletionRequest', () => { + describe('when deletionRequest is new', () => { + it('should create a new deletionRequest', async () => { + const domainObject: DeletionRequest = deletionRequestFactory.build(); + const deletionRequestId = domainObject.id; + await repo.create(domainObject); + + const result = await repo.findById(deletionRequestId); + + expect(result).toEqual(domainObject); + }); + }); + }); + + describe('findById', () => { + describe('when searching by Id', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); + await em.persistAndFlush(entity); + + const expectedDeletionRequest = { + id: entity.id, + targetRefDomain: entity.targetRefDomain, + deleteAfter: entity.deleteAfter, + targetRefId: entity.targetRefId, + status: entity.status, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + return { + entity, + expectedDeletionRequest, + }; + }; + + it('should find the deletionRequest', async () => { + const { entity, expectedDeletionRequest } = await setup(); + + const result: DeletionRequest = await repo.findById(entity.id); + + // Verify explicit fields. + expect(result).toEqual(expect.objectContaining(expectedDeletionRequest)); + }); + }); + }); + + describe('findAllItemsToExecution', () => { + describe('when there is no deletionRequest for execution', () => { + it('should return empty array', async () => { + const result = await repo.findAllItemsToExecution(); + + expect(result).toEqual([]); + }); + }); + + describe('when there are deletionRequests for execution', () => { + const setup = async () => { + const dateInFuture = new Date(); + dateInFuture.setDate(dateInFuture.getDate() + 30); + const deletionRequestEntity1: DeletionRequestEntity = deletionRequestEntityFactory.build({ + createdAt: new Date(2023, 7, 1), + deleteAfter: new Date(2023, 8, 1), + status: DeletionStatusModel.SUCCESS, + }); + const deletionRequestEntity2: DeletionRequestEntity = deletionRequestEntityFactory.build({ + createdAt: new Date(2023, 7, 1), + deleteAfter: new Date(2023, 8, 1), + status: DeletionStatusModel.FAILED, + }); + const deletionRequestEntity3: DeletionRequestEntity = deletionRequestEntityFactory.build({ + createdAt: new Date(2023, 8, 1), + deleteAfter: new Date(2023, 9, 1), + }); + const deletionRequestEntity4: DeletionRequestEntity = deletionRequestEntityFactory.build({ + createdAt: new Date(2023, 9, 1), + deleteAfter: new Date(2023, 10, 1), + }); + const deletionRequestEntity5: DeletionRequestEntity = deletionRequestEntityFactory.build({ + deleteAfter: dateInFuture, + }); + + await em.persistAndFlush([ + deletionRequestEntity1, + deletionRequestEntity2, + deletionRequestEntity3, + deletionRequestEntity4, + deletionRequestEntity5, + ]); + em.clear(); + + const expectedArray = [ + { + id: deletionRequestEntity4.id, + targetRefDomain: deletionRequestEntity4.targetRefDomain, + deleteAfter: deletionRequestEntity4.deleteAfter, + targetRefId: deletionRequestEntity4.targetRefId, + status: deletionRequestEntity4.status, + createdAt: deletionRequestEntity4.createdAt, + updatedAt: deletionRequestEntity4.updatedAt, + }, + { + id: deletionRequestEntity3.id, + targetRefDomain: deletionRequestEntity3.targetRefDomain, + deleteAfter: deletionRequestEntity3.deleteAfter, + targetRefId: deletionRequestEntity3.targetRefId, + status: deletionRequestEntity3.status, + createdAt: deletionRequestEntity3.createdAt, + updatedAt: deletionRequestEntity3.updatedAt, + }, + { + id: deletionRequestEntity2.id, + targetRefDomain: deletionRequestEntity2.targetRefDomain, + deleteAfter: deletionRequestEntity2.deleteAfter, + targetRefId: deletionRequestEntity2.targetRefId, + status: deletionRequestEntity2.status, + createdAt: deletionRequestEntity2.createdAt, + updatedAt: deletionRequestEntity2.updatedAt, + }, + ]; + + return { deletionRequestEntity1, deletionRequestEntity5, expectedArray }; + }; + + it('should find deletionRequests with deleteAfter smaller then today and status with value registered or failed', async () => { + const { deletionRequestEntity1, deletionRequestEntity5, expectedArray } = await setup(); + + const results = await repo.findAllItemsToExecution(); + + expect(results.length).toEqual(3); + + // Verify explicit fields. + expect(results).toEqual( + expect.arrayContaining([ + expect.objectContaining(expectedArray[0]), + expect.objectContaining(expectedArray[1]), + expect.objectContaining(expectedArray[2]), + ]) + ); + + const result1: DeletionRequest = await repo.findById(deletionRequestEntity1.id); + + expect(result1.id).toEqual(deletionRequestEntity1.id); + + const result5: DeletionRequest = await repo.findById(deletionRequestEntity5.id); + + expect(result5.id).toEqual(deletionRequestEntity5.id); + }); + + it('should find deletionRequests to execute with limit = 2', async () => { + const { expectedArray } = await setup(); + + const results = await repo.findAllItemsToExecution(2); + + expect(results.length).toEqual(2); + + // Verify explicit fields. + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining(expectedArray[0]), expect.objectContaining(expectedArray[1])]) + ); + }); + }); + }); + + describe('update', () => { + describe('when updating deletionRequest', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); + await em.persistAndFlush(entity); + + // Arrange expected DeletionRequestEntity after changing status + entity.status = DeletionStatusModel.SUCCESS; + const deletionRequestToUpdate = DeletionRequestMapper.mapToDO(entity); + + return { + entity, + deletionRequestToUpdate, + }; + }; + + it('should update the deletionRequest', async () => { + const { entity, deletionRequestToUpdate } = await setup(); + + await repo.update(deletionRequestToUpdate); + + const result: DeletionRequest = await repo.findById(entity.id); + + expect(result.status).toEqual(entity.status); + }); + }); + }); + + describe('markDeletionRequestAsFailed', () => { + describe('when mark deletionRequest as failed', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); + await em.persistAndFlush(entity); + + return { entity }; + }; + + it('should update the deletionRequest', async () => { + const { entity } = await setup(); + + const result = await repo.markDeletionRequestAsFailed(entity.id); + + expect(result).toBe(true); + }); + + it('should update the deletionRequest', async () => { + const { entity } = await setup(); + + await repo.markDeletionRequestAsFailed(entity.id); + + const result: DeletionRequest = await repo.findById(entity.id); + + expect(result.status).toEqual(DeletionStatusModel.FAILED); + }); + }); + }); + + describe('markDeletionRequestAsExecuted', () => { + describe('when mark deletionRequest as executed', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); + await em.persistAndFlush(entity); + + return { entity }; + }; + + it('should update the deletionRequest', async () => { + const { entity } = await setup(); + + const result = await repo.markDeletionRequestAsExecuted(entity.id); + + expect(result).toBe(true); + }); + + it('should update the deletionRequest', async () => { + const { entity } = await setup(); + + await repo.markDeletionRequestAsExecuted(entity.id); + + const result: DeletionRequest = await repo.findById(entity.id); + + expect(result.status).toEqual(DeletionStatusModel.SUCCESS); + }); + }); + }); + + describe('deleteById', () => { + describe('when deleting deletionRequest exists', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); + const deletionRequestId = entity.id; + await em.persistAndFlush(entity); + em.clear(); + + return { deletionRequestId }; + }; + + it('should delete the deletionRequest with deletionRequestId', async () => { + const { deletionRequestId } = await setup(); + + await repo.deleteById(deletionRequestId); + + expect(await em.findOne(DeletionRequestEntity, { id: deletionRequestId })).toBeNull(); + }); + + it('should return true', async () => { + const { deletionRequestId } = await setup(); + + const result: boolean = await repo.deleteById(deletionRequestId); + + expect(result).toEqual(true); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts new file mode 100644 index 00000000000..b24cf792f01 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts @@ -0,0 +1,86 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId, SortOrder } from '@shared/domain'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { DeletionRequestEntity } from '../entity'; +import { DeletionRequestMapper } from './mapper/deletion-request.mapper'; +import { DeletionRequestScope } from './deletion-request-scope'; + +@Injectable() +export class DeletionRequestRepo { + constructor(private readonly em: EntityManager) {} + + get entityName() { + return DeletionRequestEntity; + } + + async findById(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { + id: deletionRequestId, + }); + + const mapped: DeletionRequest = DeletionRequestMapper.mapToDO(deletionRequest); + + return mapped; + } + + async create(deletionRequest: DeletionRequest): Promise { + const deletionRequestEntity = DeletionRequestMapper.mapToEntity(deletionRequest); + this.em.persist(deletionRequestEntity); + await this.em.flush(); + } + + async findAllItemsToExecution(limit?: number): Promise { + const currentDate = new Date(); + const scope = new DeletionRequestScope().byDeleteAfter(currentDate).byStatus(); + const order = { createdAt: SortOrder.desc }; + + const [deletionRequestEntities] = await this.em.findAndCount(DeletionRequestEntity, scope.query, { + limit, + orderBy: order, + }); + + const mapped: DeletionRequest[] = deletionRequestEntities.map((entity) => DeletionRequestMapper.mapToDO(entity)); + + return mapped; + } + + async update(deletionRequest: DeletionRequest): Promise { + const deletionRequestEntity = DeletionRequestMapper.mapToEntity(deletionRequest); + const referencedEntity = this.em.getReference(DeletionRequestEntity, deletionRequestEntity.id); + + await this.em.persistAndFlush(referencedEntity); + } + + async markDeletionRequestAsExecuted(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { + id: deletionRequestId, + }); + + deletionRequest.executed(); + await this.em.persistAndFlush(deletionRequest); + + return true; + } + + async markDeletionRequestAsFailed(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { + id: deletionRequestId, + }); + + deletionRequest.failed(); + await this.em.persistAndFlush(deletionRequest); + + return true; + } + + async deleteById(deletionRequestId: EntityId): Promise { + const entity: DeletionRequestEntity | null = await this.em.findOneOrFail(DeletionRequestEntity, { + id: deletionRequestId, + }); + + await this.em.removeAndFlush(entity); + + return true; + } +} diff --git a/apps/server/src/modules/deletion/repo/index.ts b/apps/server/src/modules/deletion/repo/index.ts new file mode 100644 index 00000000000..68860c00a79 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-log.repo'; +export * from './deletion-request.repo'; diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts new file mode 100644 index 00000000000..a5823f5ce32 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts @@ -0,0 +1,162 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { deletionLogEntityFactory } from '../../entity/testing/factory/deletion-log.entity.factory'; +import { DeletionLogMapper } from './deletion-log.mapper'; +import { DeletionLog } from '../../domain/deletion-log.do'; +import { deletionLogFactory } from '../../domain/testing/factory/deletion-log.factory'; +import { DeletionLogEntity } from '../../entity'; + +describe(DeletionLogMapper.name, () => { + describe('mapToDO', () => { + describe('When entity is mapped for domainObject', () => { + const setup = () => { + const entity = deletionLogEntityFactory.build(); + + const expectedDomainObject = new DeletionLog({ + id: entity.id, + domain: entity.domain, + operation: entity.operation, + deletionRequestId: entity.deletionRequestId?.toHexString(), + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + + return { entity, expectedDomainObject }; + }; + it('should properly map the entity to the domain object', () => { + const { entity, expectedDomainObject } = setup(); + + const domainObject = DeletionLogMapper.mapToDO(entity); + + expect(domainObject).toEqual(expectedDomainObject); + }); + }); + }); + + describe('mapToDOs', () => { + describe('When empty entities array is mapped for an empty domainObjects array', () => { + it('should return empty domain objects array for an empty entities array', () => { + const domainObjects = DeletionLogMapper.mapToDOs([]); + + expect(domainObjects).toEqual([]); + }); + }); + + describe('When entities array is mapped for domainObjects array', () => { + const setup = () => { + const entities = [deletionLogEntityFactory.build()]; + + const expectedDomainObjects = entities.map( + (entity) => + new DeletionLog({ + id: entity.id, + domain: entity.domain, + operation: entity.operation, + deletionRequestId: entity.deletionRequestId?.toHexString(), + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }) + ); + + return { entities, expectedDomainObjects }; + }; + it('should properly map the entities to the domain objects', () => { + const { entities, expectedDomainObjects } = setup(); + + const domainObjects = DeletionLogMapper.mapToDOs(entities); + + expect(domainObjects).toEqual(expectedDomainObjects); + }); + }); + }); + + describe('mapToEntity', () => { + describe('When domainObject is mapped for entity', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + const setup = () => { + const domainObject = deletionLogFactory.build(); + + const expectedEntities = new DeletionLogEntity({ + id: domainObject.id, + domain: domainObject.domain, + operation: domainObject.operation, + deletionRequestId: new ObjectId(domainObject.deletionRequestId), + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }); + + return { domainObject, expectedEntities }; + }; + + it('should properly map the domainObject to the entity', () => { + const { domainObject, expectedEntities } = setup(); + + const entities = DeletionLogMapper.mapToEntity(domainObject); + + expect(entities).toEqual(expectedEntities); + }); + }); + }); + + describe('mapToEntities', () => { + describe('When empty domainObjects array is mapped for an entities array', () => { + it('should return empty entities array for an empty domain objects array', () => { + const entities = DeletionLogMapper.mapToEntities([]); + + expect(entities).toEqual([]); + }); + }); + + describe('When domainObjects array is mapped for entities array', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + const setup = () => { + const domainObjects = [deletionLogFactory.build()]; + + const expectedEntities = domainObjects.map( + (domainObject) => + new DeletionLogEntity({ + id: domainObject.id, + domain: domainObject.domain, + operation: domainObject.operation, + deletionRequestId: new ObjectId(domainObject.deletionRequestId), + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }) + ); + + return { domainObjects, expectedEntities }; + }; + + it('should properly map the domainObjects to the entities', () => { + const { domainObjects, expectedEntities } = setup(); + + const entities = DeletionLogMapper.mapToEntities(domainObjects); + + expect(entities).toEqual(expectedEntities); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts new file mode 100644 index 00000000000..820cd9d87c0 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts @@ -0,0 +1,39 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLogEntity } from '../../entity/deletion-log.entity'; +import { DeletionLog } from '../../domain/deletion-log.do'; + +export class DeletionLogMapper { + static mapToDO(entity: DeletionLogEntity): DeletionLog { + return new DeletionLog({ + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + domain: entity.domain, + operation: entity.operation, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, + deletionRequestId: entity.deletionRequestId?.toHexString(), + }); + } + + static mapToEntity(domainObject: DeletionLog): DeletionLogEntity { + return new DeletionLogEntity({ + id: domainObject.id, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + domain: domainObject.domain, + operation: domainObject.operation, + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, + deletionRequestId: new ObjectId(domainObject.deletionRequestId), + }); + } + + static mapToDOs(entities: DeletionLogEntity[]): DeletionLog[] { + return entities.map((entity) => this.mapToDO(entity)); + } + + static mapToEntities(domainObjects: DeletionLog[]): DeletionLogEntity[] { + return domainObjects.map((domainObject) => this.mapToEntity(domainObject)); + } +} diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts new file mode 100644 index 00000000000..4e880aab54e --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts @@ -0,0 +1,71 @@ +import { DeletionRequest } from '../../domain/deletion-request.do'; +import { deletionRequestFactory } from '../../domain/testing/factory/deletion-request.factory'; +import { DeletionRequestEntity } from '../../entity'; +import { deletionRequestEntityFactory } from '../../entity/testing/factory/deletion-request.entity.factory'; +import { DeletionRequestMapper } from './deletion-request.mapper'; + +describe(DeletionRequestMapper.name, () => { + describe('mapToDO', () => { + describe('When entity is mapped for domainObject', () => { + const setup = () => { + const entity = deletionRequestEntityFactory.build(); + + const expectedDomainObject = new DeletionRequest({ + id: entity.id, + targetRefDomain: entity.targetRefDomain, + deleteAfter: entity.deleteAfter, + targetRefId: entity.targetRefId, + status: entity.status, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + + return { entity, expectedDomainObject }; + }; + + it('should properly map the entity to the domain object', () => { + const { entity, expectedDomainObject } = setup(); + + const domainObject = DeletionRequestMapper.mapToDO(entity); + + expect(domainObject).toEqual(expectedDomainObject); + }); + }); + }); + + describe('mapToEntity', () => { + describe('When domainObject is mapped for entity', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + const setup = () => { + const domainObject = deletionRequestFactory.build(); + + const expectedEntity = new DeletionRequestEntity({ + id: domainObject.id, + targetRefDomain: domainObject.targetRefDomain, + deleteAfter: domainObject.deleteAfter, + targetRefId: domainObject.targetRefId, + status: domainObject.status, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }); + + return { domainObject, expectedEntity }; + }; + + it('should properly map the domainObject to the entity', () => { + const { domainObject, expectedEntity } = setup(); + + const entity = DeletionRequestMapper.mapToEntity(domainObject); + + expect(entity).toEqual(expectedEntity); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts new file mode 100644 index 00000000000..fd6c273011f --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts @@ -0,0 +1,28 @@ +import { DeletionRequest } from '../../domain/deletion-request.do'; +import { DeletionRequestEntity } from '../../entity'; + +export class DeletionRequestMapper { + static mapToDO(entity: DeletionRequestEntity): DeletionRequest { + return new DeletionRequest({ + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + targetRefDomain: entity.targetRefDomain, + deleteAfter: entity.deleteAfter, + targetRefId: entity.targetRefId, + status: entity.status, + }); + } + + static mapToEntity(domainObject: DeletionRequest): DeletionRequestEntity { + return new DeletionRequestEntity({ + id: domainObject.id, + targetRefDomain: domainObject.targetRefDomain, + deleteAfter: domainObject.deleteAfter, + targetRefId: domainObject.targetRefId, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + status: domainObject.status, + }); + } +} diff --git a/apps/server/src/modules/deletion/repo/mapper/index.ts b/apps/server/src/modules/deletion/repo/mapper/index.ts new file mode 100644 index 00000000000..0407135b228 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-request.mapper'; +export * from './deletion-log.mapper'; diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts new file mode 100644 index 00000000000..21522e5e924 --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts @@ -0,0 +1,110 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLogRepo } from '../repo'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionLogService } from './deletion-log.service'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; + +describe(DeletionLogService.name, () => { + let module: TestingModule; + let service: DeletionLogService; + let deletionLogRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionLogService, + { + provide: DeletionLogRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(DeletionLogService); + deletionLogRepo = module.get(DeletionLogRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('defined', () => { + it('should be defined', () => { + expect(service).toBeDefined(); + }); + }); + + describe('createDeletionRequest', () => { + describe('when creating a deletionRequest', () => { + const setup = () => { + const deletionRequestId = '653e4833cc39e5907a1e18d2'; + const domain = DeletionDomainModel.USER; + const operation = DeletionOperationModel.DELETE; + const modifiedCount = 0; + const deletedCount = 1; + + return { deletionRequestId, domain, operation, modifiedCount, deletedCount }; + }; + + it('should call deletionRequestRepo.create', async () => { + const { deletionRequestId, domain, operation, modifiedCount, deletedCount } = setup(); + + await service.createDeletionLog(deletionRequestId, domain, operation, modifiedCount, deletedCount); + + expect(deletionLogRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.any(String), + deletionRequestId, + domain, + operation, + modifiedCount, + deletedCount, + }) + ); + }); + }); + }); + + describe('findByDeletionRequestId', () => { + describe('when finding all logs for deletionRequestId', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + const deletionLog1 = deletionLogFactory.build({ deletionRequestId }); + const deletionLog2 = deletionLogFactory.build({ + deletionRequestId, + domain: DeletionDomainModel.PSEUDONYMS, + }); + const deletionLogs = [deletionLog1, deletionLog2]; + + deletionLogRepo.findAllByDeletionRequestId.mockResolvedValue(deletionLogs); + + return { deletionRequestId, deletionLogs }; + }; + + it('should call deletionLogRepo.findAllByDeletionRequestId', async () => { + const { deletionRequestId } = setup(); + await service.findByDeletionRequestId(deletionRequestId); + + expect(deletionLogRepo.findAllByDeletionRequestId).toBeCalledWith(deletionRequestId); + }); + + it('should return array of two deletionLogs with deletionRequestId', async () => { + const { deletionRequestId, deletionLogs } = setup(); + const result = await service.findByDeletionRequestId(deletionRequestId); + + expect(result).toHaveLength(2); + expect(result).toEqual(deletionLogs); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.ts b/apps/server/src/modules/deletion/services/deletion-log.service.ts new file mode 100644 index 00000000000..937d422ebb3 --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-log.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLogRepo } from '../repo'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; + +@Injectable() +export class DeletionLogService { + constructor(private readonly deletionLogRepo: DeletionLogRepo) {} + + async createDeletionLog( + deletionRequestId: EntityId, + domain: DeletionDomainModel, + operation: DeletionOperationModel, + modifiedCount: number, + deletedCount: number + ): Promise { + const newDeletionLog = new DeletionLog({ + id: new ObjectId().toHexString(), + domain, + deletionRequestId, + operation, + modifiedCount, + deletedCount, + }); + + await this.deletionLogRepo.create(newDeletionLog); + } + + async findByDeletionRequestId(deletionRequestId: EntityId): Promise { + const deletionLogs: DeletionLog[] = await this.deletionLogRepo.findAllByDeletionRequestId(deletionRequestId); + + return deletionLogs; + } +} diff --git a/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts new file mode 100644 index 00000000000..fcccfc433db --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts @@ -0,0 +1,200 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { setupEntities } from '@shared/testing'; +import { DeletionRequestService } from './deletion-request.service'; +import { DeletionRequestRepo } from '../repo'; +import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +describe(DeletionRequestService.name, () => { + let module: TestingModule; + let service: DeletionRequestService; + let deletionRequestRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionRequestService, + { + provide: DeletionRequestRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(DeletionRequestService); + deletionRequestRepo = module.get(DeletionRequestRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('defined', () => { + it('should be defined', () => { + expect(service).toBeDefined(); + }); + }); + + describe('createDeletionRequest', () => { + describe('when creating a deletionRequest', () => { + const setup = () => { + const targetRefId = '653e4833cc39e5907a1e18d2'; + const targetRefDomain = DeletionDomainModel.USER; + + return { targetRefId, targetRefDomain }; + }; + + it('should call deletionRequestRepo.create', async () => { + const { targetRefId, targetRefDomain } = setup(); + + await service.createDeletionRequest(targetRefId, targetRefDomain); + + expect(deletionRequestRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.any(String), + targetRefDomain, + deleteAfter: expect.any(Date), + targetRefId, + status: DeletionStatusModel.REGISTERED, + }) + ); + }); + }); + }); + + describe('findById', () => { + describe('when finding by deletionRequestId', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + const deletionRequest = deletionRequestFactory.build({ id: deletionRequestId }); + + deletionRequestRepo.findById.mockResolvedValue(deletionRequest); + + return { deletionRequestId, deletionRequest }; + }; + + it('should call deletionRequestRepo.findById', async () => { + const { deletionRequestId } = setup(); + + await service.findById(deletionRequestId); + + expect(deletionRequestRepo.findById).toBeCalledWith(deletionRequestId); + }); + + it('should return deletionRequest', async () => { + const { deletionRequestId, deletionRequest } = setup(); + + const result = await service.findById(deletionRequestId); + + expect(result).toEqual(deletionRequest); + }); + }); + }); + + describe('findAllItemsToExecute', () => { + describe('when finding all deletionRequests for execution', () => { + const setup = () => { + const dateInPast = new Date(); + dateInPast.setDate(dateInPast.getDate() - 1); + const deletionRequest1 = deletionRequestFactory.build({ deleteAfter: dateInPast }); + const deletionRequest2 = deletionRequestFactory.build({ deleteAfter: dateInPast }); + + deletionRequestRepo.findAllItemsToExecution.mockResolvedValue([deletionRequest1, deletionRequest2]); + + const deletionRequests = [deletionRequest1, deletionRequest2]; + return { deletionRequests }; + }; + + it('should call deletionRequestRepo.findAllItemsByDeletionDate', async () => { + await service.findAllItemsToExecute(); + + expect(deletionRequestRepo.findAllItemsToExecution).toBeCalled(); + }); + + it('should return array of two deletionRequests to execute', async () => { + const { deletionRequests } = setup(); + const result = await service.findAllItemsToExecute(); + + expect(result).toHaveLength(2); + expect(result).toEqual(deletionRequests); + }); + }); + }); + + describe('update', () => { + describe('when updating deletionRequest', () => { + const setup = () => { + const deletionRequest = deletionRequestFactory.buildWithId(); + + return { deletionRequest }; + }; + + it('should call deletionRequestRepo.update', async () => { + const { deletionRequest } = setup(); + await service.update(deletionRequest); + + expect(deletionRequestRepo.update).toBeCalledWith(deletionRequest); + }); + }); + }); + + describe('markDeletionRequestAsExecuted', () => { + describe('when mark deletionRequest as executed', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + + return { deletionRequestId }; + }; + + it('should call deletionRequestRepo.markDeletionRequestAsExecuted', async () => { + const { deletionRequestId } = setup(); + await service.markDeletionRequestAsExecuted(deletionRequestId); + + expect(deletionRequestRepo.markDeletionRequestAsExecuted).toBeCalledWith(deletionRequestId); + }); + }); + }); + + describe('markDeletionRequestAsFailed', () => { + describe('when mark deletionRequest as failed', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + + return { deletionRequestId }; + }; + + it('should call deletionRequestRepo.markDeletionRequestAsExecuted', async () => { + const { deletionRequestId } = setup(); + await service.markDeletionRequestAsFailed(deletionRequestId); + + expect(deletionRequestRepo.markDeletionRequestAsFailed).toBeCalledWith(deletionRequestId); + }); + }); + }); + + describe('deleteById', () => { + describe('when deleting deletionRequest', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + + return { deletionRequestId }; + }; + + it('should call deletionRequestRepo.findAllItemsByDeletionDate', async () => { + const { deletionRequestId } = setup(); + await service.deleteById(deletionRequestId); + + expect(deletionRequestRepo.deleteById).toBeCalledWith(deletionRequestId); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/deletion-request.service.ts b/apps/server/src/modules/deletion/services/deletion-request.service.ts new file mode 100644 index 00000000000..82b65521d68 --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequestRepo } from '../repo/deletion-request.repo'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +@Injectable() +export class DeletionRequestService { + constructor(private readonly deletionRequestRepo: DeletionRequestRepo) {} + + async createDeletionRequest( + targetRefId: EntityId, + targetRefDomain: DeletionDomainModel, + deleteInMinutes = 43200 + ): Promise<{ requestId: EntityId; deletionPlannedAt: Date }> { + const dateOfDeletion = new Date(); + dateOfDeletion.setMinutes(dateOfDeletion.getMinutes() + deleteInMinutes); + + const newDeletionRequest = new DeletionRequest({ + id: new ObjectId().toHexString(), + targetRefDomain, + deleteAfter: dateOfDeletion, + targetRefId, + status: DeletionStatusModel.REGISTERED, + }); + + await this.deletionRequestRepo.create(newDeletionRequest); + + return { requestId: newDeletionRequest.id, deletionPlannedAt: newDeletionRequest.deleteAfter }; + } + + async findById(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequest = await this.deletionRequestRepo.findById(deletionRequestId); + + return deletionRequest; + } + + async findAllItemsToExecute(limit?: number): Promise { + const itemsToDelete: DeletionRequest[] = await this.deletionRequestRepo.findAllItemsToExecution(limit); + + return itemsToDelete; + } + + async update(deletionRequestToUpdate: DeletionRequest): Promise { + await this.deletionRequestRepo.update(deletionRequestToUpdate); + } + + async markDeletionRequestAsExecuted(deletionRequestId: EntityId): Promise { + return this.deletionRequestRepo.markDeletionRequestAsExecuted(deletionRequestId); + } + + async markDeletionRequestAsFailed(deletionRequestId: EntityId): Promise { + return this.deletionRequestRepo.markDeletionRequestAsFailed(deletionRequestId); + } + + async deleteById(deletionRequestId: EntityId): Promise { + await this.deletionRequestRepo.deleteById(deletionRequestId); + } +} diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts new file mode 100644 index 00000000000..9661354718c --- /dev/null +++ b/apps/server/src/modules/deletion/services/index.ts @@ -0,0 +1 @@ +export * from './deletion-request.service'; diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts new file mode 100644 index 00000000000..c2952f40f59 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts @@ -0,0 +1,22 @@ +import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; +import { DeletionLogStatisticBuilder } from './deletion-log-statistic.builder'; + +describe(DeletionLogStatisticBuilder.name, () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should build generic deletionLogStatistic with all attributes', () => { + // Arrange + const domain = DeletionDomainModel.PSEUDONYMS; + const modifiedCount = 0; + const deletedCount = 2; + + const result = DeletionLogStatisticBuilder.build(domain, modifiedCount, deletedCount); + + // Assert + expect(result.domain).toEqual(domain); + expect(result.modifiedCount).toEqual(modifiedCount); + expect(result.deletedCount).toEqual(deletedCount); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts b/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts new file mode 100644 index 00000000000..a562505b885 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts @@ -0,0 +1,10 @@ +import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; +import { DeletionLogStatistic } from '../interface'; + +export class DeletionLogStatisticBuilder { + static build(domain: DeletionDomainModel, modifiedCount?: number, deletedCount?: number): DeletionLogStatistic { + const deletionLogStatistic = { domain, modifiedCount, deletedCount }; + + return deletionLogStatistic; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts new file mode 100644 index 00000000000..b317a4b2221 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts @@ -0,0 +1,28 @@ +import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; +import { DeletionLogStatisticBuilder } from './deletion-log-statistic.builder'; +import { DeletionRequestLogBuilder } from './deletion-request-log.builder'; +import { DeletionTargetRefBuilder } from './deletion-target-ref.builder'; + +describe(DeletionRequestLogBuilder.name, () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should build generic deletionRequestLog with all attributes', () => { + // Arrange + const targetRefDomain = DeletionDomainModel.PSEUDONYMS; + const targetRefId = '653e4833cc39e5907a1e18d2'; + const targetRef = DeletionTargetRefBuilder.build(targetRefDomain, targetRefId); + const deletionPlannedAt = new Date(); + const modifiedCount = 0; + const deletedCount = 2; + const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, modifiedCount, deletedCount)]; + + const result = DeletionRequestLogBuilder.build(targetRef, deletionPlannedAt, statistics); + + // Assert + expect(result.targetRef).toEqual(targetRef); + expect(result.deletionPlannedAt).toEqual(deletionPlannedAt); + expect(result.statistics).toEqual(statistics); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts b/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts new file mode 100644 index 00000000000..8247acf6776 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts @@ -0,0 +1,13 @@ +import { DeletionLogStatistic, DeletionRequestLog, DeletionTargetRef } from '../interface'; + +export class DeletionRequestLogBuilder { + static build( + targetRef: DeletionTargetRef, + deletionPlannedAt: Date, + statistics?: DeletionLogStatistic[] + ): DeletionRequestLog { + const deletionRequestLog = { targetRef, deletionPlannedAt, statistics }; + + return deletionRequestLog; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts new file mode 100644 index 00000000000..2fb4ae440a7 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts @@ -0,0 +1,20 @@ +import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; +import { DeletionTargetRefBuilder } from './deletion-target-ref.builder'; + +describe(DeletionTargetRefBuilder.name, () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should build generic deletionTargetRef with all attributes', () => { + // Arrange + const domain = DeletionDomainModel.PSEUDONYMS; + const refId = '653e4833cc39e5907a1e18d2'; + + const result = DeletionTargetRefBuilder.build(domain, refId); + + // Assert + expect(result.targetRefDomain).toEqual(domain); + expect(result.targetRefId).toEqual(refId); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts b/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts new file mode 100644 index 00000000000..91f3385a9aa --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts @@ -0,0 +1,11 @@ +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; +import { DeletionTargetRef } from '../interface'; + +export class DeletionTargetRefBuilder { + static build(targetRefDomain: DeletionDomainModel, targetRefId: EntityId): DeletionTargetRef { + const deletionTargetRef = { targetRefDomain, targetRefId }; + + return deletionTargetRef; + } +} 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 new file mode 100644 index 00000000000..063f3d46b48 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts @@ -0,0 +1,459 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { setupEntities } from '@shared/testing'; +import { AccountService } from '@modules/account/services'; +import { ClassService } from '@modules/class'; +import { CourseGroupService, CourseService } from '@modules/learnroom/service'; +import { FilesService } from '@modules/files/service'; +import { LessonService } from '@modules/lesson/service'; +import { PseudonymService } from '@modules/pseudonym'; +import { TeamService } from '@modules/teams'; +import { UserService } from '@modules/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'; + +describe(DeletionRequestUc.name, () => { + let module: TestingModule; + let uc: DeletionRequestUc; + let deletionRequestService: DeepMocked; + let deletionLogService: DeepMocked; + let accountService: DeepMocked; + let classService: DeepMocked; + let courseGroupService: DeepMocked; + let courseService: DeepMocked; + let filesService: DeepMocked; + let lessonService: DeepMocked; + let pseudonymService: DeepMocked; + let teamService: DeepMocked; + let userService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionRequestUc, + { + provide: DeletionRequestService, + useValue: createMock(), + }, + { + provide: DeletionLogService, + useValue: createMock(), + }, + { + provide: AccountService, + useValue: createMock(), + }, + { + provide: ClassService, + useValue: createMock(), + }, + { + provide: CourseGroupService, + useValue: createMock(), + }, + { + provide: CourseService, + useValue: createMock(), + }, + { + provide: FilesService, + useValue: createMock(), + }, + { + provide: LessonService, + useValue: createMock(), + }, + { + provide: PseudonymService, + useValue: createMock(), + }, + { + provide: TeamService, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(DeletionRequestUc); + deletionRequestService = module.get(DeletionRequestService); + deletionLogService = module.get(DeletionLogService); + accountService = module.get(AccountService); + classService = module.get(ClassService); + courseGroupService = module.get(CourseGroupService); + courseService = module.get(CourseService); + filesService = module.get(FilesService); + lessonService = module.get(LessonService); + pseudonymService = module.get(PseudonymService); + teamService = module.get(TeamService); + userService = module.get(UserService); + await setupEntities(); + }); + + describe('createDeletionRequest', () => { + describe('when creating a deletionRequest', () => { + const setup = () => { + jest.clearAllMocks(); + const deletionRequestToCreate: DeletionRequestProps = { + targetRef: { + targetRefDoamin: DeletionDomainModel.USER, + targetRefId: '653e4833cc39e5907a1e18d2', + }, + deleteInMinutes: 1440, + }; + const deletionRequest = deletionRequestFactory.build(); + + return { + deletionRequestToCreate, + deletionRequest, + }; + }; + + it('should call the service to create the deletionRequest', async () => { + const { deletionRequestToCreate } = setup(); + + await uc.createDeletionRequest(deletionRequestToCreate); + + expect(deletionRequestService.createDeletionRequest).toHaveBeenCalledWith( + deletionRequestToCreate.targetRef.targetRefId, + deletionRequestToCreate.targetRef.targetRefDoamin, + deletionRequestToCreate.deleteInMinutes + ); + }); + + it('should return the deletionRequestID and deletionPlannedAt', async () => { + const { deletionRequestToCreate, deletionRequest } = setup(); + + deletionRequestService.createDeletionRequest.mockResolvedValueOnce({ + requestId: deletionRequest.id, + deletionPlannedAt: deletionRequest.deleteAfter, + }); + + const result = await uc.createDeletionRequest(deletionRequestToCreate); + + expect(result).toEqual({ + requestId: deletionRequest.id, + deletionPlannedAt: deletionRequest.deleteAfter, + }); + }); + }); + }); + + describe('executeDeletionRequests', () => { + describe('when executing deletionRequests', () => { + const setup = () => { + jest.clearAllMocks(); + const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); + + classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); + courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); + courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2); + filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(2); + filesService.removeUserPermissionsToAnyFiles.mockResolvedValueOnce(2); + lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(2); + pseudonymService.deleteByUserId.mockResolvedValueOnce(2); + teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); + userService.deleteUser.mockResolvedValueOnce(1); + + return { + deletionRequestToExecute, + }; + }; + + it('should call deletionRequestService.findAllItemsToExecute', async () => { + await uc.executeDeletionRequests(); + + expect(deletionRequestService.findAllItemsToExecute).toHaveBeenCalled(); + }); + + it('should call deletionRequestService.markDeletionRequestAsExecuted to update status of deletionRequests', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(deletionRequestService.markDeletionRequestAsExecuted).toHaveBeenCalledWith(deletionRequestToExecute.id); + }); + + it('should call accountService.deleteByUserId to delete user data in account module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(accountService.deleteByUserId).toHaveBeenCalled(); + }); + + it('should call classService.deleteUserDataFromClasses to delete user data in class module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(classService.deleteUserDataFromClasses).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call courseGroupService.deleteUserDataFromCourseGroup to delete user data in courseGroup module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(courseGroupService.deleteUserDataFromCourseGroup).toHaveBeenCalledWith( + deletionRequestToExecute.targetRefId + ); + }); + + it('should call courseService.deleteUserDataFromCourse to delete user data in course module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(courseService.deleteUserDataFromCourse).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call filesService.markFilesOwnedByUserForDeletion to mark users files to delete in file module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(filesService.markFilesOwnedByUserForDeletion).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call filesService.removeUserPermissionsToAnyFiles to remove users permissions to any files in file module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(filesService.removeUserPermissionsToAnyFiles).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call lessonService.deleteUserDataFromLessons to delete users data in lesson module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(lessonService.deleteUserDataFromLessons).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call pseudonymService.deleteByUserId to delete users data in pseudonym module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(pseudonymService.deleteByUserId).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call teamService.deleteUserDataFromTeams to delete users data in teams module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(teamService.deleteUserDataFromTeams).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call userService.deleteUsers to delete user in user module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(userService.deleteUser).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); + }); + + it('should call deletionLogService.createDeletionLog to create logs for deletionRequest', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(9); + }); + }); + + describe('when an error occurred', () => { + const setup = () => { + jest.clearAllMocks(); + const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); + + classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); + courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); + courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2); + filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(2); + filesService.removeUserPermissionsToAnyFiles.mockResolvedValueOnce(2); + lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(2); + pseudonymService.deleteByUserId.mockResolvedValueOnce(2); + teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); + userService.deleteUser.mockRejectedValueOnce(new Error()); + + return { + deletionRequestToExecute, + }; + }; + + it('should throw an arror', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(deletionRequestService.markDeletionRequestAsFailed).toHaveBeenCalledWith(deletionRequestToExecute.id); + }); + }); + }); + + describe('findById', () => { + describe('when searching for logs for deletionRequest which was executed', () => { + const setup = () => { + jest.clearAllMocks(); + const deletionRequestExecuted = deletionRequestFactory.build({ status: DeletionStatusModel.SUCCESS }); + const deletionLogExecuted1 = deletionLogFactory.build({ deletionRequestId: deletionRequestExecuted.id }); + const deletionLogExecuted2 = deletionLogFactory.build({ + deletionRequestId: deletionRequestExecuted.id, + domain: DeletionDomainModel.ACCOUNT, + modifiedCount: 0, + deletedCount: 1, + }); + + const executedDeletionRequestSummary: DeletionRequestLog = { + targetRef: { + targetRefDomain: deletionRequestExecuted.targetRefDomain, + targetRefId: deletionRequestExecuted.targetRefId, + }, + deletionPlannedAt: deletionRequestExecuted.deleteAfter, + statistics: [ + { + domain: deletionLogExecuted1.domain, + modifiedCount: deletionLogExecuted1.modifiedCount, + deletedCount: deletionLogExecuted1.deletedCount, + }, + { + domain: deletionLogExecuted2.domain, + modifiedCount: deletionLogExecuted2.modifiedCount, + deletedCount: deletionLogExecuted2.deletedCount, + }, + ], + }; + + return { + deletionRequestExecuted, + executedDeletionRequestSummary, + deletionLogExecuted1, + deletionLogExecuted2, + }; + }; + + it('should call to deletionRequestService and deletionLogService', async () => { + const { deletionRequestExecuted } = setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequestExecuted); + + await uc.findById(deletionRequestExecuted.id); + + expect(deletionRequestService.findById).toHaveBeenCalledWith(deletionRequestExecuted.id); + expect(deletionLogService.findByDeletionRequestId).toHaveBeenCalledWith(deletionRequestExecuted.id); + }); + + it('should return object with summary of deletionRequest', async () => { + const { deletionRequestExecuted, deletionLogExecuted1, deletionLogExecuted2, executedDeletionRequestSummary } = + setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequestExecuted); + deletionLogService.findByDeletionRequestId.mockResolvedValueOnce([deletionLogExecuted1, deletionLogExecuted2]); + + const result = await uc.findById(deletionRequestExecuted.id); + + expect(result).toEqual(executedDeletionRequestSummary); + }); + }); + + describe('when searching for logs for deletionRequest which was not executed', () => { + const setup = () => { + jest.clearAllMocks(); + const deletionRequest = deletionRequestFactory.build(); + const notExecutedDeletionRequestSummary: DeletionRequestLog = { + targetRef: { + targetRefDomain: deletionRequest.targetRefDomain, + targetRefId: deletionRequest.targetRefId, + }, + deletionPlannedAt: deletionRequest.deleteAfter, + }; + + return { + deletionRequest, + notExecutedDeletionRequestSummary, + }; + }; + + it('should call to deletionRequestService', async () => { + const { deletionRequest } = setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequest); + + await uc.findById(deletionRequest.id); + + expect(deletionRequestService.findById).toHaveBeenCalledWith(deletionRequest.id); + expect(deletionLogService.findByDeletionRequestId).not.toHaveBeenCalled(); + }); + + it('should return object with summary of deletionRequest', async () => { + const { deletionRequest, notExecutedDeletionRequestSummary } = setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequest); + + const result = await uc.findById(deletionRequest.id); + + expect(result).toEqual(notExecutedDeletionRequestSummary); + }); + }); + }); + + describe('deleteDeletionRequestById', () => { + describe('when deleting a deletionRequestId', () => { + const setup = () => { + jest.clearAllMocks(); + const deletionRequest = deletionRequestFactory.build(); + + return { + deletionRequest, + }; + }; + + it('should call the service deletionRequestService.deleteById', async () => { + const { deletionRequest } = setup(); + + await uc.deleteDeletionRequestById(deletionRequest.id); + + expect(deletionRequestService.deleteById).toHaveBeenCalledWith(deletionRequest.id); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts new file mode 100644 index 00000000000..d94a129310f --- /dev/null +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -0,0 +1,209 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { PseudonymService } from '@modules/pseudonym'; +import { UserService } from '@modules/user'; +import { TeamService } from '@modules/teams'; +import { ClassService } from '@modules/class'; +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 { DeletionRequestService } from '../services/deletion-request.service'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionLogService } from '../services/deletion-log.service'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { + DeletionRequestProps, + DeletionRequestLog, + DeletionLogStatistic, + DeletionRequestCreateAnswer, +} from './interface/interfaces'; +import { DeletionLogStatisticBuilder } from './builder/deletion-log-statistic.builder'; +import { DeletionRequestLogBuilder } from './builder/deletion-request-log.builder'; +import { DeletionTargetRefBuilder } from './builder/deletion-target-ref.builder'; + +@Injectable() +export class DeletionRequestUc { + constructor( + private readonly deletionRequestService: DeletionRequestService, + private readonly deletionLogService: DeletionLogService, + private readonly accountService: AccountService, + private readonly classService: ClassService, + private readonly courseGroupService: CourseGroupService, + private readonly courseService: CourseService, + private readonly filesService: FilesService, + private readonly lessonService: LessonService, + private readonly pseudonymService: PseudonymService, + private readonly teamService: TeamService, + private readonly userService: UserService + ) {} + + async createDeletionRequest(deletionRequest: DeletionRequestProps): Promise { + const result = await this.deletionRequestService.createDeletionRequest( + deletionRequest.targetRef.targetRefId, + deletionRequest.targetRef.targetRefDoamin, + deletionRequest.deleteInMinutes + ); + + return result; + } + + async executeDeletionRequests(limit?: number): Promise { + const deletionRequestToExecution: DeletionRequest[] = await this.deletionRequestService.findAllItemsToExecute( + limit + ); + + for (const req of deletionRequestToExecution) { + // eslint-disable-next-line no-await-in-loop + await this.executeDeletionRequest(req); + } + } + + async findById(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequest = await this.deletionRequestService.findById(deletionRequestId); + let response: DeletionRequestLog = DeletionRequestLogBuilder.build( + DeletionTargetRefBuilder.build(deletionRequest.targetRefDomain, deletionRequest.targetRefId), + deletionRequest.deleteAfter + ); + + if (deletionRequest.status === DeletionStatusModel.SUCCESS) { + const deletionLog: DeletionLog[] = await this.deletionLogService.findByDeletionRequestId(deletionRequestId); + const deletionLogStatistic: DeletionLogStatistic[] = deletionLog.map((log) => + DeletionLogStatisticBuilder.build(log.domain, log.modifiedCount, log.deletedCount) + ); + response = { ...response, statistics: deletionLogStatistic }; + } + + return response; + } + + async deleteDeletionRequestById(deletionRequestId: EntityId): Promise { + await this.deletionRequestService.deleteById(deletionRequestId); + } + + private async executeDeletionRequest(deletionRequest: DeletionRequest): Promise { + try { + await Promise.all([ + this.removeAccount(deletionRequest), + this.removeUserFromClasses(deletionRequest), + this.removeUserFromCourseGroup(deletionRequest), + this.removeUserFromCourse(deletionRequest), + this.removeUsersFilesAndPermissions(deletionRequest), + this.removeUserFromLessons(deletionRequest), + this.removeUsersPseudonyms(deletionRequest), + this.removeUserFromTeams(deletionRequest), + this.removeUser(deletionRequest), + ]); + await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); + } catch (error) { + await this.deletionRequestService.markDeletionRequestAsFailed(deletionRequest.id); + } + } + + private async logDeletion( + deletionRequest: DeletionRequest, + domainModel: DeletionDomainModel, + operationModel: DeletionOperationModel, + updatedCount: number, + deletedCount: number + ): Promise { + if (updatedCount > 0 || deletedCount > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + domainModel, + operationModel, + updatedCount, + deletedCount + ); + } + } + + private async removeAccount(deletionRequest: DeletionRequest) { + await this.accountService.deleteByUserId(deletionRequest.targetRefId); + await this.logDeletion(deletionRequest, DeletionDomainModel.ACCOUNT, DeletionOperationModel.DELETE, 0, 1); + } + + private async removeUserFromClasses(deletionRequest: DeletionRequest) { + const classesUpdated: number = await this.classService.deleteUserDataFromClasses(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.CLASS, + DeletionOperationModel.UPDATE, + classesUpdated, + 0 + ); + } + + private async removeUserFromCourseGroup(deletionRequest: DeletionRequest) { + const courseGroupUpdated: number = await this.courseGroupService.deleteUserDataFromCourseGroup( + deletionRequest.targetRefId + ); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.COURSEGROUP, + DeletionOperationModel.UPDATE, + courseGroupUpdated, + 0 + ); + } + + private async removeUserFromCourse(deletionRequest: DeletionRequest) { + const courseUpdated: number = await this.courseService.deleteUserDataFromCourse(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.COURSE, + DeletionOperationModel.UPDATE, + courseUpdated, + 0 + ); + } + + private async removeUsersFilesAndPermissions(deletionRequest: DeletionRequest) { + const filesDeleted: number = await this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.targetRefId); + const filePermissionsUpdated: number = await this.filesService.removeUserPermissionsToAnyFiles( + deletionRequest.targetRefId + ); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.FILE, + DeletionOperationModel.UPDATE, + filesDeleted + filePermissionsUpdated, + 0 + ); + } + + private async removeUserFromLessons(deletionRequest: DeletionRequest) { + const lessonsUpdated: number = await this.lessonService.deleteUserDataFromLessons(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.LESSONS, + DeletionOperationModel.UPDATE, + lessonsUpdated, + 0 + ); + } + + private async removeUsersPseudonyms(deletionRequest: DeletionRequest) { + const pseudonymDeleted: number = await this.pseudonymService.deleteByUserId(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + DeletionDomainModel.PSEUDONYMS, + DeletionOperationModel.DELETE, + 0, + pseudonymDeleted + ); + } + + private async removeUserFromTeams(deletionRequest: DeletionRequest) { + const teamsUpdated: number = await this.teamService.deleteUserDataFromTeams(deletionRequest.targetRefId); + await this.logDeletion(deletionRequest, DeletionDomainModel.TEAMS, DeletionOperationModel.UPDATE, teamsUpdated, 0); + } + + private async removeUser(deletionRequest: DeletionRequest) { + const userDeleted: number = await this.userService.deleteUser(deletionRequest.targetRefId); + await this.logDeletion(deletionRequest, DeletionDomainModel.USER, DeletionOperationModel.DELETE, 0, userDeleted); + } +} diff --git a/apps/server/src/modules/deletion/uc/interface/index.ts b/apps/server/src/modules/deletion/uc/interface/index.ts new file mode 100644 index 00000000000..95786098275 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/index.ts @@ -0,0 +1 @@ +export * from './interfaces'; diff --git a/apps/server/src/modules/deletion/uc/interface/interfaces.ts b/apps/server/src/modules/deletion/uc/interface/interfaces.ts new file mode 100644 index 00000000000..47f4d887735 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/interfaces.ts @@ -0,0 +1,29 @@ +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; + +export interface DeletionTargetRef { + targetRefDomain: DeletionDomainModel; + targetRefId: EntityId; +} + +export interface DeletionRequestLog { + targetRef: DeletionTargetRef; + deletionPlannedAt: Date; + statistics?: DeletionLogStatistic[]; +} + +export interface DeletionLogStatistic { + domain: DeletionDomainModel; + modifiedCount?: number; + deletedCount?: number; +} + +export interface DeletionRequestProps { + targetRef: { targetRefDoamin: DeletionDomainModel; targetRefId: EntityId }; + deleteInMinutes?: number; +} + +export interface DeletionRequestCreateAnswer { + requestId: EntityId; + deletionPlannedAt: Date; +} diff --git a/apps/server/src/modules/learnroom/service/index.ts b/apps/server/src/modules/learnroom/service/index.ts index 608249cbf43..ca9d75634cf 100644 --- a/apps/server/src/modules/learnroom/service/index.ts +++ b/apps/server/src/modules/learnroom/service/index.ts @@ -4,3 +4,4 @@ export * from './column-board-target.service'; export * from './common-cartridge-export.service'; export * from './course.service'; export * from './rooms.service'; +export * from './coursegroup.service'; From aff2019b2cb4ca6e54f3b82cd9ad722e1a3458df Mon Sep 17 00:00:00 2001 From: Phillip Date: Tue, 7 Nov 2023 19:46:51 +0100 Subject: [PATCH 03/23] BC-4887 fix sorting by _id (#4527) --- apps/server/src/modules/account/repo/account.repo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/account/repo/account.repo.ts b/apps/server/src/modules/account/repo/account.repo.ts index d1872d1ca67..f8c1f5677ec 100644 --- a/apps/server/src/modules/account/repo/account.repo.ts +++ b/apps/server/src/modules/account/repo/account.repo.ts @@ -71,7 +71,7 @@ export class AccountRepo extends BaseRepo { * @deprecated For migration purpose only */ async findMany(offset = 0, limit = 100): Promise { - const result = await this._em.find(this.entityName, {}, { offset, limit, orderBy: { id: SortOrder.asc } }); + const result = await this._em.find(this.entityName, {}, { offset, limit, orderBy: { _id: SortOrder.asc } }); this._em.clear(); return result; } From 301324fbe2d724cb829214791741fcf983311e08 Mon Sep 17 00:00:00 2001 From: Caspar Neumann <146704428+casparneumann-cap@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:37:12 +0100 Subject: [PATCH 04/23] Thr 18 dev feature h5p editor (#4525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * THR-1 h5p editor: temporary file storage (#4147) * Created H5P microservice * Add kubernetes files for h5p microservice * Added @lumieducation/h5p-server package * Don't use mock auth for testing * Fix api tests to use new TestApiClient * WIP: H5P editor: temp file storage * WIP: H5P editor: added data classes * resolve merge conflict leftovers * h5p editor: temp file storage implementation completed * h5p editor: temp file storage tests added * h5p editor: temp file storage moved * h5p editor: temp storage files moved * Update temporary-file-storage.ts Create temporary file folder --------- Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> Co-authored-by: Andre Blome Co-authored-by: Stephan Krause <101647440+SteKrause@users.noreply.github.com> * THR-2 local content storage (#4140) * Created H5P microservice * Add kubernetes files for h5p microservice * Added @lumieducation/h5p-server package * add ContentStorage with interface IContentStorage * add addContent() * implement addFile(), contentExists(), deleteContent() * implement deleteFile(), fileExists() * add getFileStats(), getFileStream() * add getMetadata(), getParameters(), getUsage() * add getUserPermissions(), listContent() * add listFiles(), sanitizeFilename() * refactor sanitizeFileneame * add user check for getMetadata and getParameters * fix existsOrCreateDir * fix deleteContent and fileExists * fix fileExists and contentExists, refactor methods * add unit tests for contentStorage * delete comments in addFile * fix addFile test and change descriptions * fix getUsage * fix getUsage test * delete TODO * add error message to contentExists + tests * add test for empty contentId at fileEsists * remove unused private sanitize method * adjust errorhandling + add tests for error cases * add tests and fix error cases * replace math.random * change regex in checkfilename * refactor tests and content storage implementation * change any values to unknown * fix create content id * refactor and fix exist boolean in createContentId * fix test can not createContentId * create folder if content will be created * Allow files in subdirectories --------- Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> * THR-3 local library storage (#4133) * Implemented library storage * Tests for librray storage * full test coverage * Moved files --------- Co-authored-by: Stephan Krause <101647440+SteKrause@users.noreply.github.com> * h5p editor: added deployment which uses existing template (#4136) Co-authored-by: Andre Blome Co-authored-by: Stephan Krause <101647440+SteKrause@users.noreply.github.com> * Thr 5 h5p implementation endpoints (#4169) * Created H5P microservice * Add kubernetes files for h5p microservice * Added @lumieducation/h5p-server package * Don't use mock auth for testing * add ContentStorage with interface IContentStorage * Fix api tests to use new TestApiClient * WIP: H5P editor: temp file storage * WIP: H5P editor: added data classes * add addContent() * implement addFile(), contentExists(), deleteContent() * implement deleteFile(), fileExists() * add getFileStats(), getFileStream() * add getMetadata(), getParameters(), getUsage() * add getUserPermissions(), listContent() * add listFiles(), sanitizeFilename() * refactor sanitizeFileneame * Implemented library storage * add user check for getMetadata and getParameters * fix existsOrCreateDir * h5p editor: added deployment which uses existing template * fix deleteContent and fileExists * Tests for librray storage * resolve merge conflict leftovers * fix fileExists and contentExists, refactor methods * add unit tests for contentStorage * delete comments in addFile * fix addFile test and change descriptions * fix getUsage * fix getUsage test * delete TODO * full test coverage * Moved files * Implemented library storage * Tests for librray storage * full test coverage * Moved files * add error message to contentExists + tests * add test for empty contentId at fileEsists * remove unused private sanitize method * adjust errorhandling + add tests for error cases * add tests and fix error cases * replace math.random * change regex in checkfilename * delete unused code * change regex in checkfilename * refactor tests and content storage implementation * change any values to unknown * fix create content id * Fix issues from merging main into THR-2 * refactor and fix exist boolean in createContentId * fix test can not createContentId * Stubbed endpoints * Editor and Player service * Added H5P Usecase * h5p editor: temp file storage implementation completed * h5p editor: temp file storage tests added * h5p editor: temp file storage moved * h5p editor: temp storage files moved * File streaming * Post Ajax endpoint * Merge branch 'THR-8-h5p-api-endpoints' of github.com:hpi-schul-cloud/schulcloud-server into THR-5-h5p-implementation-endpoints * Remove @nestjs/serve-static dependency * Editor working more reliably * Disable saving of userstate * Organized code * More organization * Tests for getAjax usecase * add tests for h5p controller * Tests and organization * add import of FilesStorageAMQPModule to h5p-editor module * Switched over to storage implementations * Organized modules * change stringPath of createNewEditor * Testing for files in UC * Testing content parameters in UC * change AjaxPostBodyParams to undefined if empty * change save api endpoint * Fixed file streaming * Fixed h5p uploading * Update temporary-file-storage.ts Create temporary file folder * merge save and create endpoints to one get and post endpoint * create folder if content will be created * adjust paths at api tests * rename usecase to getH5pEditor * Use real user * Allow files in subdirectories * Testing for h5p files * API tests for internal AJAX endpoint * Test range requests for storages * add unit-tests to usecases * add api tests for h5p endpoints * add api test save or create * adjust create or save api test --------- Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> Co-authored-by: Andre Blome * THR-7 ContentStorage: store data on S3 via S3ClientAdapter (#4198) * Add "List" and "Head" Methods to S3ClientAdapter * H5P Content Storage now uses S3 Bucket and database * Added database repo for H5P metadata * Thr 20 create UI elements (#4190) * Necessary changes to support H5P on the client * h5p editor: library storage s3 implementation * h5p editor: library storage updated; unit tests; bugfixes * change to correct Buckets / Key names * add config keys * change default value for env var S3_REGION * Fix API Validation * THR-25 S3 implementation of temporary file storage (#4204) * Created H5P microservice * Add kubernetes files for h5p microservice * Added @lumieducation/h5p-server package * Don't use mock auth for testing * Fix api tests to use new TestApiClient * WIP: H5P editor: temp file storage * WIP: H5P editor: added data classes * resolve merge conflict leftovers * h5p editor: temp file storage implementation completed * h5p editor: temp file storage tests added * h5p editor: temp file storage moved * h5p editor: temp storage files moved * h5p editor: temp file storage changed to use s3 * h5p editor: s3 config and adapter injection handled * h5p editor: temp file storage fixes * h5p editor: temp file storage: repo method renamed to findByUserAndFilename() * change to correct Buckets names * change to correct Bucket name * initial commit * Merge remote-tracking branch 'origin/THR-18-dev-feature-h5p-editor' into THR-25-temp-storage-s3 * Update configs * Update module and fix all tests * Fix module config * Fix Library Entity from THR-18 * Updated Tests * Remove console output * Remove config from development.json * Remove unused config file * Organized all files for H5P module --------- Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> Co-authored-by: Andre Blome Co-authored-by: Majed Mak <132336669+MajedAlaitwniCap@users.noreply.github.com> Co-authored-by: Majed Aitwni * Thr 27 h5p language parameter (#4283) * adjust controller and uc for language params * refactor h5p-editor service * use userService in getUserLanguage() * use env available languages * Refactor merging * resolve bugs in uc tests * change h5p-editor dto * delete await from editor + player declarations --------- Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> Co-authored-by: Andre Blome Co-authored-by: Majed Aitwni * THR-42 fixes to library storage (#4332) * Library storage THR-34 hotfixes * Fix small bugs in LibraryStorageService * Update Tests * resolved PR comments * delete h5p-editor static files * Remove deployment from main * remove static server * add LibraryStorage to uc tests * expect LanguageType in editor uc test * add missing language param at api tests * add field name to response type IGetFileResponse * change library entity constructor * refactor library entity constructor * add list of files (#4349) * add list of files * change s3 command type * fix tests * move recursive part to private methode * fix limits --------- Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> * code Smells corrector * THR-6 H5P editor authorization (#4364) * Preparing entities for authorization * Add entities * save and create tests * get player tests * get editor tests * files uc tests * last tests for changed UC * better error message * Document custom transform pipe * Fix PR comments * BaseEntityWithTimestamps for entities * Address more PR comments * pr comments * pr comments * Better file handling * Missing file * fix changes from s3 module * h5p editor: change api and static files urls * refactor library tests and imports * refactor imports of contentStorage and temporary file storage * refactor h5p content tests * delete unused imports * refactor h5p api tests * refactor imports in module * delete blank line * restructure api tests h5p-save-create * h5p editor: temporarily dont map errors * refactor tests regarding authorization * fix save-create api tests * quick fixes service tests * fix test in contentStorage service * refactor api editor files tests * set default parameter rangeStart = 0 * minor code structure adjustments * refactor: only one return statement * fix commit dd41731: redefine type of returnValue * resolve type error for response stream * revert minor code changes * review comments controller * Revert "review comments controller" * Fix s3 client mocks * Fix tests * fix file storage service test * Fix integration tests * add empty line before return * refactor: await below ifelse block * refactor: set default for rangeStart * Merge remote-tracking branch 'origin/main' into THR-18-dev-feature-h5p-editor * Fix imports * Remove import from files-storage * do not overwrite input parameter * change Error Type to NotAcceptable * create own h5pfile dto * fix remaining review comments in temporary-file-storage.service * refactor h5peditor/ h5pplayer service to provider Hint: npm ci * add test for new Repo Method * fix unit test for Temporary-file-storage * fix test Module imports * fix review comments: return method and error types * create seperate h5p error mapper - unit tests still WIP * update unit tests for uc.getAjax & uc.postAjax * resolve ts expect error & Test corrector * resolve rest review comments h5p.uc.ts * remove unused import * move PostBodyParamsTransformPipe to extra file * rename TemporaryFile to BaseEntityWithTimestamp * add private method and use cause error * refactor from Service to H5PAjaxEndpointProvider * use explicit return type * use cause error types * initial commit * create library.repo.spec test setup and first test * create tests for library.repo * revert * revert zu main * last review comments * fix library.repo.spec code coverage * Add library.entity.spec * Add test for existsOne to increase CodeCoverage * add h5p-content mapper Test * Fix eslint * fix Test * fix saveFile mockImplementation * fix unnecessary type assertion * import Correction * add tests libraryStorage.service * remove uneccessary * Remove uneccessary import * correction and add test for h5p mapper * remove uneccessary imports * remove unused import * test structure * add tests for library.entity * WIP | create tests post.body.params.transform-pipe * add tests for post.body.params.transform-pipe * remove unused parameter * Create Test for H5PContentMetadata * refactor Method * define method return type * Add test for temporary-file-storage.service * remove unused import * added H5p File Response interface * create tests h5p-content.entity * add GetLibraryFile dto * rename entity file * Create own GetLibraryFile Dto for UC * Revert post.body.params.transform-pipe & logging * bugfix post.body.params.transform-pipe * add return undefined * quick fix: transformed can be undefined * adapt userMcok to updated ICurrentUser Interface * Restructure AjaxPostBodyParamsTransformPipe * quick fix nest-cli.json * revert wrong Changes * Comment out end-to-end tests in push * Update push.yml * Update push.yml: add legacy e2e test again --------- Co-authored-by: André Blome <43345275+ssmid@users.noreply.github.com> Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> Co-authored-by: Andre Blome Co-authored-by: Stephan Krause <101647440+SteKrause@users.noreply.github.com> Co-authored-by: Majed Mak <132336669+MajedAlaitwniCap@users.noreply.github.com> Co-authored-by: stekrause Co-authored-by: Majed Aitwni Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> Co-authored-by: SevenWaysDP --- .github/workflows/push.yml | 2 +- .../schulcloud-server-h5p/tasks/main.yml | 2 +- apps/server/src/apps/h5p-editor.app.ts | 1 + .../api-test/h5p-editor-ajax.api.spec.ts | 178 ++++ .../api-test/h5p-editor-delete.api.spec.ts | 106 ++ .../api-test/h5p-editor-files.api.spec.ts | 381 +++++++ .../h5p-editor-get-editor.api.spec.ts | 155 +++ .../h5p-editor-get-player.api.spec.ts | 114 +++ .../h5p-editor-save-create.api.spec.ts | 176 ++++ .../api-test/h5p-editor.api.spec.ts | 106 -- .../controller/dto/ajax/get.params.ts | 23 + .../h5p-editor/controller/dto/ajax/index.ts | 3 + .../post.body.params.transform-pipe.spec.ts | 79 ++ .../ajax/post.body.params.transform-pipe.ts | 39 + .../controller/dto/ajax/post.body.params.ts | 28 + .../controller/dto/ajax/post.params.ts | 27 + .../controller/dto/content-file.url.params.ts | 13 + .../dto/h5p-editor-response.spec.ts | 26 + .../controller/dto/h5p-editor.params.ts | 82 ++ .../controller/dto/h5p-editor.response.ts | 94 ++ .../h5p-editor/controller/dto/h5p-file.dto.ts | 32 + .../h5p-editor/controller/dto/index.ts | 6 + .../controller/dto/library-file.url.params.ts | 14 + .../controller/h5p-editor.controller.ts | 240 ++++- .../entity/h5p-content.entity.spec.ts | 42 + .../h5p-editor/entity/h5p-content.entity.ts | 163 +++ .../entity/h5p-editor-tempfile.entity.ts | 41 + .../src/modules/h5p-editor/entity/index.ts | 3 + .../h5p-editor/entity/library.entity.spec.ts | 223 +++++ .../h5p-editor/entity/library.entity.ts | 249 +++++ .../h5p-editor/h5p-editor-test.module.ts | 38 +- .../modules/h5p-editor/h5p-editor.config.ts | 26 + .../modules/h5p-editor/h5p-editor.module.ts | 38 +- .../mapper/h5p-content.mapper.spec.ts | 20 + .../h5p-editor/mapper/h5p-content.mapper.ts | 19 + .../mapper/h5p-error.mapper.spec.ts | 26 + .../h5p-editor/mapper/h5p-error.mapper.ts | 8 + .../provider/h5p-ajax-endpoint.provider.ts | 11 + .../provider/h5p-editor.provider.ts | 37 + .../provider/h5p-player.provider.ts | 27 + .../src/modules/h5p-editor/provider/index.ts | 3 + .../repo/h5p-content.repo.integration.spec.ts | 104 ++ .../h5p-editor/repo/h5p-content.repo.ts | 29 + .../src/modules/h5p-editor/repo/index.ts | 3 + .../h5p-editor/repo/library.repo.spec.ts | 178 ++++ .../modules/h5p-editor/repo/library.repo.ts | 78 ++ .../temporary-file.repo.integration.spec.ts | 127 +++ .../h5p-editor/repo/temporary-file.repo.ts | 33 + .../service/config/h5p-service-config.ts | 27 + .../service/contentStorage.service.spec.ts | 928 ++++++++++++++++++ .../service/contentStorage.service.ts | 305 ++++++ .../service/h5p-translator.service.ts | 34 + .../src/modules/h5p-editor/service/index.ts | 4 + .../service/libraryStorage.service.spec.ts | 765 +++++++++++++++ .../service/libraryStorage.service.ts | 452 +++++++++ .../temporary-file-storage.service.spec.ts | 309 ++++++ .../service/temporary-file-storage.service.ts | 124 +++ .../modules/h5p-editor/types/lumi-types.ts | 45 + .../h5p-editor/uc/dto/h5p-getLibraryFile.ts | 8 + .../modules/h5p-editor/uc/h5p-ajax.uc.spec.ts | 227 +++++ .../h5p-editor/uc/h5p-delete.uc.spec.ts | 188 ++++ .../h5p-editor/uc/h5p-files.uc.spec.ts | 592 +++++++++++ .../h5p-editor/uc/h5p-get-editor.uc.spec.ts | 278 ++++++ .../h5p-editor/uc/h5p-get-player.uc.spec.ts | 198 ++++ .../h5p-editor/uc/h5p-save-create.uc.spec.ts | 340 +++++++ .../src/modules/h5p-editor/uc/h5p.uc.ts | 410 ++++++++ .../shared/infra/s3-client/interface/index.ts | 14 + .../infra/s3-client/s3-client.adapter.spec.ts | 215 +++- .../infra/s3-client/s3-client.adapter.ts | 72 +- .../testing/factory/h5p-content.factory.ts | 36 + .../factory/h5p-temporary-file.factory.ts | 25 + .../src/shared/testing/factory/index.ts | 2 + config/development.json | 7 + package-lock.json | 62 +- package.json | 2 + 75 files changed, 8951 insertions(+), 171 deletions(-) create mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts delete mode 100644 apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/ajax/get.params.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/ajax/index.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/ajax/post.params.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/content-file.url.params.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/h5p-editor-response.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.params.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.response.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/index.ts create mode 100644 apps/server/src/modules/h5p-editor/controller/dto/library-file.url.params.ts create mode 100644 apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts create mode 100644 apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts create mode 100644 apps/server/src/modules/h5p-editor/entity/index.ts create mode 100644 apps/server/src/modules/h5p-editor/entity/library.entity.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/entity/library.entity.ts create mode 100644 apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.ts create mode 100644 apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.ts create mode 100644 apps/server/src/modules/h5p-editor/provider/h5p-ajax-endpoint.provider.ts create mode 100644 apps/server/src/modules/h5p-editor/provider/h5p-editor.provider.ts create mode 100644 apps/server/src/modules/h5p-editor/provider/h5p-player.provider.ts create mode 100644 apps/server/src/modules/h5p-editor/provider/index.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/h5p-content.repo.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/index.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/library.repo.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/repo/temporary-file.repo.ts create mode 100644 apps/server/src/modules/h5p-editor/service/config/h5p-service-config.ts create mode 100644 apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/service/contentStorage.service.ts create mode 100644 apps/server/src/modules/h5p-editor/service/h5p-translator.service.ts create mode 100644 apps/server/src/modules/h5p-editor/service/index.ts create mode 100644 apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts create mode 100644 apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts create mode 100644 apps/server/src/modules/h5p-editor/types/lumi-types.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/dto/h5p-getLibraryFile.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p-ajax.uc.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts create mode 100644 apps/server/src/modules/h5p-editor/uc/h5p.uc.ts create mode 100644 apps/server/src/shared/testing/factory/h5p-content.factory.ts create mode 100644 apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index c042be2c2a9..a5688568696 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -172,7 +172,7 @@ jobs: uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - + end-to-end-tests: needs: - build_and_push diff --git a/ansible/roles/schulcloud-server-h5p/tasks/main.yml b/ansible/roles/schulcloud-server-h5p/tasks/main.yml index 368e97a216e..0cb4feff19c 100644 --- a/ansible/roles/schulcloud-server-h5p/tasks/main.yml +++ b/ansible/roles/schulcloud-server-h5p/tasks/main.yml @@ -1,4 +1,4 @@ - - name: H5pEditorService + - name: H5PEditorProvider kubernetes.core.k8s: kubeconfig: ~/.kube/config namespace: "{{ NAMESPACE }}" diff --git a/apps/server/src/apps/h5p-editor.app.ts b/apps/server/src/apps/h5p-editor.app.ts index 518eb5c45bc..c25f9156be6 100644 --- a/apps/server/src/apps/h5p-editor.app.ts +++ b/apps/server/src/apps/h5p-editor.app.ts @@ -19,6 +19,7 @@ async function bootstrap() { const nestExpress = express(); const nestExpressAdapter = new ExpressAdapter(nestExpress); + const nestApp = await NestFactory.create(H5PEditorModule, nestExpressAdapter); // WinstonLogger nestApp.useLogger(await nestApp.resolve(LegacyLogger)); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts new file mode 100644 index 00000000000..543db38ebbf --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts @@ -0,0 +1,178 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PAjaxEndpoint } from '@lumieducation/h5p-server'; +import { EntityManager } from '@mikro-orm/core'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { H5PEditorTestModule } from '../../h5p-editor-test.module'; +import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; + +describe('H5PEditor Controller (api)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + let ajaxEndpoint: DeepMocked; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [H5PEditorTestModule], + }) + .overrideProvider(H5P_CONTENT_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5P_LIBRARIES_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5PAjaxEndpoint) + .useValue(createMock()) + .compile(); + + app = module.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + ajaxEndpoint = app.get(H5PAjaxEndpoint); + testApiClient = new TestApiClient(app, 'h5p-editor'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('when calling AJAX GET', () => { + describe('when user not exists', () => { + it('should respond with unauthorized exception', async () => { + const response = await testApiClient.get('ajax'); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + type: 'UNAUTHORIZED', + title: 'Unauthorized', + message: 'Unauthorized', + code: 401, + }); + }); + }); + + describe('when user is logged in', () => { + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + + const setup = async () => { + const { studentAccount, studentUser } = createStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient, studentUser }; + }; + + it('should call H5PAjaxEndpoint', async () => { + const { + loggedInClient, + studentUser: { id }, + } = await setup(); + + const dummyResponse = { + apiVersion: { major: 1, minor: 1 }, + details: [], + libraries: [], + outdated: false, + recentlyUsed: [], + user: 'DummyUser', + }; + + ajaxEndpoint.getAjax.mockResolvedValueOnce(dummyResponse); + + const response = await loggedInClient.get(`ajax?action=content-type-cache`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual(dummyResponse); + expect(ajaxEndpoint.getAjax).toHaveBeenCalledWith( + 'content-type-cache', + undefined, // MachineName + undefined, // MajorVersion + undefined, // MinorVersion + 'de', // Language + expect.objectContaining({ id }) + ); + }); + }); + + describe('when calling AJAX POST', () => { + describe('when user not exists', () => { + it('should respond with unauthorized exception', async () => { + const response = await testApiClient.post('ajax'); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + type: 'UNAUTHORIZED', + title: 'Unauthorized', + message: 'Unauthorized', + code: 401, + }); + }); + }); + + describe('when user is logged in', () => { + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + + const setup = async () => { + const { studentAccount, studentUser } = createStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient, studentUser }; + }; + + it('should call H5PAjaxEndpoint', async () => { + const { + loggedInClient, + studentUser: { id }, + } = await setup(); + + const dummyResponse = [ + { + majorVersion: 1, + minorVersion: 2, + metadataSettings: {}, + name: 'Dummy Library', + restricted: false, + runnable: true, + title: 'Dummy Library', + tutorialUrl: '', + uberName: 'dummyLibrary-1.1', + }, + ]; + + const dummyBody = { contentId: 'id', field: 'field', libraries: ['dummyLibrary-1.0'], libraryParameters: '' }; + + ajaxEndpoint.postAjax.mockResolvedValueOnce(dummyResponse); + + const response = await loggedInClient.post(`ajax?action=libraries`, dummyBody); + + expect(response.statusCode).toEqual(HttpStatus.CREATED); + expect(response.body).toEqual(dummyResponse); + expect(ajaxEndpoint.postAjax).toHaveBeenCalledWith( + 'libraries', + dummyBody, + 'de', + expect.objectContaining({ id }), + undefined, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts new file mode 100644 index 00000000000..9f3b0017d08 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts @@ -0,0 +1,106 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Permission } from '@shared/domain'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; +import { Request } from 'express'; +import request from 'supertest'; +import { H5PEditorTestModule } from '../../h5p-editor-test.module'; +import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; +import { H5PEditorUc } from '../../uc/h5p.uc'; + +class API { + constructor(private app: INestApplication) { + this.app = app; + } + + async deleteH5pContent(contentId: string) { + return request(this.app.getHttpServer()).post(`/h5p-editor/delete/${contentId}`); + } +} + +const setup = () => { + const contentId = new ObjectId(0).toString(); + const notExistingContentId = new ObjectId(1).toString(); + const badContentId = ''; + + return { contentId, notExistingContentId, badContentId }; +}; + +describe('H5PEditor Controller (api)', () => { + let app: INestApplication; + let api: API; + let em: EntityManager; + let currentUser: ICurrentUser; + let h5PEditorUc: DeepMocked; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [H5PEditorTestModule], + }) + .overrideGuard(JwtAuthGuard) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.user = currentUser; + return true; + }, + }) + .overrideProvider(H5P_CONTENT_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5P_LIBRARIES_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5PEditorUc) + .useValue(createMock()) + .compile(); + + app = module.createNestApplication(); + await app.init(); + h5PEditorUc = module.get(H5PEditorUc); + + api = new API(app); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('delete h5p content', () => { + beforeEach(async () => { + await cleanupCollections(em); + const school = schoolFactory.build(); + const roles = roleFactory.buildList(1, { + permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], + }); + const user = userFactory.build({ school, roles }); + + await em.persistAndFlush([user, school]); + em.clear(); + + currentUser = mapUserToCurrentUser(user); + }); + describe('with valid request params', () => { + it('should return 200 status', async () => { + const { contentId } = setup(); + + h5PEditorUc.deleteH5pContent.mockResolvedValueOnce(true); + const response = await api.deleteH5pContent(contentId); + expect(response.status).toEqual(201); + }); + }); + describe('with bad request params', () => { + it('should return 500 status', async () => { + const { notExistingContentId } = setup(); + + h5PEditorUc.deleteH5pContent.mockRejectedValueOnce(new Error('Could not delete H5P content')); + const response = await api.deleteH5pContent(notExistingContentId); + expect(response.status).toEqual(500); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts new file mode 100644 index 00000000000..702aac1a717 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts @@ -0,0 +1,381 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ILibraryName } from '@lumieducation/h5p-server'; +import { ContentMetadata } from '@lumieducation/h5p-server/build/src/ContentMetadata'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { + courseFactory, + h5pContentFactory, + lessonFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { ObjectID } from 'bson'; +import { Readable } from 'stream'; +import { H5PContent, H5PContentParentType, IH5PContentProperties, H5pEditorTempFile } from '../../entity'; +import { H5PEditorTestModule } from '../../h5p-editor-test.module'; +import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; +import { ContentStorage, LibraryStorage, TemporaryFileStorage } from '../../service'; + +const helpers = { + buildMetadata( + title: string, + mainLibrary: string, + preloadedDependencies: ILibraryName[] = [], + dynamicDependencies?: ILibraryName[], + editorDependencies?: ILibraryName[] + ): ContentMetadata { + return { + defaultLanguage: 'de-DE', + license: 'Unlicensed', + title, + dynamicDependencies, + editorDependencies, + embedTypes: ['iframe'], + language: 'de-DE', + mainLibrary, + preloadedDependencies, + }; + }, + + buildContent(n = 0) { + const metadata = helpers.buildMetadata(`Content #${n}`, `Library-${n}.0`); + const content = { + data: `Data #${n}`, + }; + const h5pContentProperties: IH5PContentProperties = { + creatorId: new ObjectID().toString(), + parentId: new ObjectID().toString(), + schoolId: new ObjectID().toString(), + metadata, + content, + parentType: H5PContentParentType.Lesson, + }; + const h5pContent = new H5PContent(h5pContentProperties); + + return { + withID(id?: number) { + const objectId = new ObjectID(id); + h5pContent._id = objectId; + h5pContent.id = objectId.toString(); + + return h5pContent; + }, + new() { + return h5pContent; + }, + }; + }, +}; + +describe('H5PEditor Controller (api)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + let contentStorage: DeepMocked; + let libraryStorage: DeepMocked; + let temporaryStorage: DeepMocked; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [H5PEditorTestModule], + }) + .overrideProvider(H5P_CONTENT_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5P_LIBRARIES_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(ContentStorage) + .useValue(createMock()) + .overrideProvider(LibraryStorage) + .useValue(createMock()) + .overrideProvider(TemporaryFileStorage) + .useValue(createMock()) + .compile(); + + app = module.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + contentStorage = app.get(ContentStorage); + libraryStorage = app.get(LibraryStorage); + temporaryStorage = app.get(TemporaryFileStorage); + testApiClient = new TestApiClient(app, 'h5p-editor'); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('when requesting library files', () => { + describe('when user not exists', () => { + it('should respond with unauthorized exception', async () => { + const response = await testApiClient.get('libraries/dummyLib/test.txt'); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + type: 'UNAUTHORIZED', + title: 'Unauthorized', + message: 'Unauthorized', + code: 401, + }); + }); + }); + + describe('when user is logged in', () => { + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + + const setup = async () => { + const { studentAccount, studentUser } = createStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient }; + }; + + it('should return the library file', async () => { + const { loggedInClient } = await setup(); + + const mockFile = { content: 'Test File', size: 9, name: 'test.txt', birthtime: new Date() }; + + libraryStorage.getLibraryFile.mockResolvedValueOnce({ + stream: Readable.from(mockFile.content), + size: mockFile.size, + mimetype: 'text/plain', + }); + + const response = await loggedInClient.get(`libraries/dummyLib-1.0/${mockFile.name}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.text).toBe(mockFile.content); + }); + + it('should return 404 if file does not exist', async () => { + const { loggedInClient } = await setup(); + + libraryStorage.getLibraryFile.mockRejectedValueOnce(new Error('Does not exist')); + + const response = await loggedInClient.get(`libraries/dummyLib-1.0/nonexistant.txt`); + + expect(response.statusCode).toEqual(HttpStatus.NOT_FOUND); + }); + }); + }); + + describe('when requesting content files', () => { + describe('when user not exists', () => { + it('should respond with unauthorized exception', async () => { + const response = await testApiClient.get('content/dummyId/test.txt'); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + type: 'UNAUTHORIZED', + title: 'Unauthorized', + message: 'Unauthorized', + code: 401, + }); + }); + }); + + describe('when user is logged in', () => { + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + + const setup = async () => { + const { studentAccount, studentUser } = createStudent(); + const course = courseFactory.build({ students: [studentUser], school: studentUser.school }); + const lesson = lessonFactory.build({ course }); + await em.persistAndFlush([studentAccount, studentUser, lesson, course]); + + const content = h5pContentFactory.build({ parentId: lesson.id, parentType: H5PContentParentType.Lesson }); + await em.persistAndFlush([content]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient, content }; + }; + + it('should return the content file', async () => { + const { loggedInClient, content } = await setup(); + + const mockFile = { content: 'Test File', size: 9, name: 'test.txt', birthtime: new Date() }; + + contentStorage.getFileStream.mockResolvedValueOnce(Readable.from(mockFile.content)); + contentStorage.getFileStats.mockResolvedValueOnce({ birthtime: mockFile.birthtime, size: mockFile.size }); + + const response = await loggedInClient.get(`content/${content.id}/${mockFile.name}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.text).toBe(mockFile.content); + }); + + it('should work with range requests', async () => { + const { loggedInClient, content } = await setup(); + + const mockFile = { content: 'Test File', size: 9, name: 'test.txt', birthtime: new Date() }; + + contentStorage.getFileStream.mockResolvedValueOnce(Readable.from(mockFile.content)); + contentStorage.getFileStats.mockResolvedValueOnce({ birthtime: mockFile.birthtime, size: mockFile.size }); + + const response = await loggedInClient.get(`content/${content.id}/${mockFile.name}`).set('Range', 'bytes=2-4'); + + expect(response.statusCode).toEqual(HttpStatus.PARTIAL_CONTENT); + expect(response.text).toBe(mockFile.content); + }); + + it('should return 404 if file does not exist', async () => { + const { loggedInClient, content } = await setup(); + + contentStorage.getFileStats.mockRejectedValueOnce(new Error('Does not exist')); + + const response = await loggedInClient.get(`content/${content.id}/nonexistant.txt`); + + expect(response.statusCode).toEqual(HttpStatus.NOT_FOUND); + }); + }); + }); + + describe('when requesting temporary files', () => { + describe('when user not exists', () => { + it('should respond with unauthorized exception', async () => { + const response = await testApiClient.get('temp-files/test.txt'); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + type: 'UNAUTHORIZED', + title: 'Unauthorized', + message: 'Unauthorized', + code: 401, + }); + }); + }); + + describe('when user is logged in', () => { + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + + const setup = async () => { + const { studentAccount, studentUser } = createStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const mockFile = { + name: 'example.txt', + content: 'File Content', + }; + + const mockTempFile = new H5pEditorTempFile({ + filename: mockFile.name, + ownedByUserId: studentUser.id, + expiresAt: new Date(), + birthtime: new Date(), + size: mockFile.content.length, + }); + + return { loggedInClient, mockFile, mockTempFile }; + }; + + it('should return the content file', async () => { + const { loggedInClient, mockFile, mockTempFile } = await setup(); + + temporaryStorage.getFileStream.mockResolvedValueOnce(Readable.from(mockFile.content)); + temporaryStorage.getFileStats.mockResolvedValueOnce(mockTempFile); + + const response = await loggedInClient.get(`temp-files/${mockFile.name}`); + + expect(response.statusCode).toEqual(HttpStatus.PARTIAL_CONTENT); + expect(response.text).toBe(mockFile.content); + }); + + it('should work with range requests', async () => { + const { loggedInClient, mockFile, mockTempFile } = await setup(); + + temporaryStorage.getFileStream.mockResolvedValueOnce(Readable.from(mockFile.content)); + temporaryStorage.getFileStats.mockResolvedValueOnce(mockTempFile); + + const response = await loggedInClient.get(`temp-files/${mockFile.name}`).set('Range', 'bytes=2-4'); + + expect(response.statusCode).toEqual(HttpStatus.PARTIAL_CONTENT); + expect(response.text).toBe(mockFile.content); + }); + + it('should return 404 if file does not exist', async () => { + const { loggedInClient } = await setup(); + + temporaryStorage.getFileStats.mockRejectedValueOnce(new Error('Does not exist')); + + const response = await loggedInClient.get(`temp-files/nonexistant.txt`); + + expect(response.statusCode).toEqual(HttpStatus.NOT_FOUND); + }); + }); + }); + + describe('when requesting content parameters', () => { + describe('when user not exists', () => { + it('should respond with unauthorized exception', async () => { + const response = await testApiClient.get('params/dummyId'); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + type: 'UNAUTHORIZED', + title: 'Unauthorized', + message: 'Unauthorized', + code: 401, + }); + }); + }); + + describe('when user is logged in', () => { + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + + const setup = async () => { + const { studentAccount, studentUser } = createStudent(); + const course = courseFactory.build({ students: [studentUser], school: studentUser.school }); + const lesson = lessonFactory.build({ course }); + await em.persistAndFlush([studentAccount, studentUser, lesson, course]); + + const content = h5pContentFactory.build({ parentId: lesson.id, parentType: H5PContentParentType.Lesson }); + await em.persistAndFlush([content]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient, content }; + }; + + it('should return the content parameters', async () => { + const { loggedInClient, content } = await setup(); + + const dummyMetadata = new ContentMetadata(); + const dummyParams = { name: 'Dummy' }; + + contentStorage.getMetadata.mockResolvedValueOnce(dummyMetadata); + contentStorage.getParameters.mockResolvedValueOnce(dummyParams); + const response = await loggedInClient.get(`params/${content.id}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + h5p: dummyMetadata, + params: { metadata: dummyMetadata, params: dummyParams }, + }); + }); + + it('should return 404 if content does not exist', async () => { + const { loggedInClient } = await setup(); + + contentStorage.getMetadata.mockRejectedValueOnce(new Error('Does not exist')); + + const response = await loggedInClient.get('params/dummyId'); + + expect(response.statusCode).toEqual(HttpStatus.NOT_FOUND); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts new file mode 100644 index 00000000000..737266f300d --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts @@ -0,0 +1,155 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Permission } from '@shared/domain'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; +import { Request } from 'express'; +import request from 'supertest'; +import { H5PEditorTestModule } from '../../h5p-editor-test.module'; +import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; +import { H5PEditorUc } from '../../uc/h5p.uc'; + +class API { + constructor(private app: INestApplication) { + this.app = app; + } + + async emptyEditor() { + return request(this.app.getHttpServer()).get(`/h5p-editor/edit/de`); + } + + async editH5pContent(contentId: string) { + return request(this.app.getHttpServer()).get(`/h5p-editor/edit/${contentId}/de`); + } +} + +const setup = () => { + const contentId = new ObjectId(0).toString(); + const notExistingContentId = new ObjectId(1).toString(); + const badContentId = ''; + + const editorModel = { + scripts: ['example.js'], + styles: ['example.css'], + }; + + const exampleContent = { + h5p: {}, + library: 'ExampleLib-1.0', + params: { + metadata: {}, + params: { anything: true }, + }, + }; + + return { contentId, notExistingContentId, badContentId, editorModel, exampleContent }; +}; + +describe('H5PEditor Controller (api)', () => { + let app: INestApplication; + let api: API; + let em: EntityManager; + let currentUser: ICurrentUser; + let h5PEditorUc: DeepMocked; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [H5PEditorTestModule], + }) + .overrideGuard(JwtAuthGuard) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.user = currentUser; + return true; + }, + }) + .overrideProvider(H5P_CONTENT_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5P_LIBRARIES_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5PEditorUc) + .useValue(createMock()) + .compile(); + + app = module.createNestApplication(); + await app.init(); + h5PEditorUc = module.get(H5PEditorUc); + + api = new API(app); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('get new h5p editor', () => { + beforeEach(async () => { + await cleanupCollections(em); + const school = schoolFactory.build(); + const roles = roleFactory.buildList(1, { + permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], + }); + const user = userFactory.build({ school, roles }); + + await em.persistAndFlush([user, school]); + em.clear(); + + currentUser = mapUserToCurrentUser(user); + }); + describe('with valid request params', () => { + it('should return 200 status', async () => { + const { editorModel } = setup(); + // @ts-expect-error partial object + h5PEditorUc.getEmptyH5pEditor.mockResolvedValueOnce(editorModel); + const response = await api.emptyEditor(); + expect(response.status).toEqual(200); + }); + }); + describe('with bad request params', () => { + it('should return 500 status', async () => { + h5PEditorUc.getEmptyH5pEditor.mockRejectedValueOnce(new Error('Could not get H5P editor')); + const response = await api.emptyEditor(); + expect(response.status).toEqual(500); + }); + }); + }); + + describe('get h5p editor', () => { + beforeEach(async () => { + await cleanupCollections(em); + const school = schoolFactory.build(); + const roles = roleFactory.buildList(1, { + permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], + }); + const user = userFactory.build({ school, roles }); + + await em.persistAndFlush([user, school]); + em.clear(); + + currentUser = mapUserToCurrentUser(user); + }); + describe('with valid request params', () => { + it('should return 200 status', async () => { + const { contentId, editorModel, exampleContent } = setup(); + // @ts-expect-error partial object + h5PEditorUc.getH5pEditor.mockResolvedValueOnce({ editorModel, content: exampleContent }); + const response = await api.editH5pContent(contentId); + expect(response.status).toEqual(200); + }); + }); + describe('with bad request params', () => { + it('should return 500 status', async () => { + const { notExistingContentId } = setup(); + h5PEditorUc.getH5pEditor.mockRejectedValueOnce(new Error('Could not get H5P editor')); + const response = await api.editH5pContent(notExistingContentId); + expect(response.status).toEqual(500); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts new file mode 100644 index 00000000000..708dfef968a --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts @@ -0,0 +1,114 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { IPlayerModel } from '@lumieducation/h5p-server'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Permission } from '@shared/domain'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; +import { Request } from 'express'; +import request from 'supertest'; +import { H5PEditorTestModule } from '../../h5p-editor-test.module'; +import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; +import { H5PEditorUc } from '../../uc/h5p.uc'; + +class API { + constructor(private app: INestApplication) { + this.app = app; + } + + async getPlayer(contentId: string) { + return request(this.app.getHttpServer()).get(`/h5p-editor/play/${contentId}`); + } +} + +const setup = () => { + const contentId = new ObjectId(0).toString(); + const notExistingContentId = new ObjectId(1).toString(); + + // @ts-expect-error partial object + const playerResult: IPlayerModel = { + contentId, + dependencies: [], + downloadPath: '', + embedTypes: ['iframe'], + scripts: ['example.js'], + styles: ['example.css'], + }; + + return { contentId, notExistingContentId, playerResult }; +}; + +describe('H5PEditor Controller (api)', () => { + let app: INestApplication; + let api: API; + let em: EntityManager; + let currentUser: ICurrentUser; + let h5PEditorUc: DeepMocked; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [H5PEditorTestModule], + }) + .overrideGuard(JwtAuthGuard) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.user = currentUser; + return true; + }, + }) + .overrideProvider(H5P_CONTENT_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5P_LIBRARIES_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5PEditorUc) + .useValue(createMock()) + .compile(); + + app = module.createNestApplication(); + h5PEditorUc = module.get(H5PEditorUc); + await app.init(); + + api = new API(app); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('get h5p player', () => { + beforeEach(async () => { + await cleanupCollections(em); + const school = schoolFactory.build(); + const roles = roleFactory.buildList(1, { + permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], + }); + const user = userFactory.build({ school, roles }); + + await em.persistAndFlush([user, school]); + em.clear(); + + currentUser = mapUserToCurrentUser(user); + }); + describe('with valid request params', () => { + it('should return 200 status', async () => { + const { contentId, playerResult } = setup(); + h5PEditorUc.getH5pPlayer.mockResolvedValueOnce(playerResult); + const response = await api.getPlayer(contentId); + expect(response.status).toEqual(200); + }); + }); + describe('with bad request params', () => { + it('should return 500 status', async () => { + const { notExistingContentId } = setup(); + h5PEditorUc.getH5pPlayer.mockRejectedValueOnce(new Error('Could not get H5P player')); + const response = await api.getPlayer(notExistingContentId); + expect(response.status).toEqual(500); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts new file mode 100644 index 00000000000..9981a13e81f --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts @@ -0,0 +1,176 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { IContentMetadata } from '@lumieducation/h5p-server'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { H5PContentParentType } from '../../entity'; +import { H5PEditorTestModule } from '../../h5p-editor-test.module'; +import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; +import { H5PEditorUc } from '../../uc/h5p.uc'; +import { PostH5PContentCreateParams } from '../dto'; + +describe('H5PEditor Controller (api)', () => { + let app: INestApplication; + let em: EntityManager; + let h5PEditorUc: DeepMocked; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [H5PEditorTestModule], + }) + .overrideProvider(H5P_CONTENT_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5P_LIBRARIES_S3_CONNECTION) + .useValue(createMock()) + .overrideProvider(H5PEditorUc) + .useValue(createMock()) + .compile(); + + app = module.createNestApplication(); + await app.init(); + h5PEditorUc = module.get(H5PEditorUc); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, 'h5p-editor'); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('create h5p content', () => { + describe('with valid request params', () => { + const setup = async () => { + const id = '0000000'; + const metadata: IContentMetadata = { + embedTypes: [], + language: 'de', + mainLibrary: 'mainLib', + preloadedDependencies: [], + defaultLanguage: '', + license: '', + title: '123', + }; + const params: PostH5PContentCreateParams = { + parentType: H5PContentParentType.Lesson, + parentId: new ObjectId().toString(), + params: { + params: undefined, + metadata: { + embedTypes: [], + language: '', + mainLibrary: '', + preloadedDependencies: [], + defaultLanguage: '', + license: '', + title: '', + }, + }, + library: '123', + }; + + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + const { studentAccount, studentUser } = createStudent(); + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + const loggedInClient = await testApiClient.login(studentAccount); + + return { id, metadata, loggedInClient, params }; + }; + it('should return 201 status', async () => { + const { id, metadata, loggedInClient, params } = await setup(); + const result1 = { id, metadata }; + h5PEditorUc.createH5pContentGetMetadata.mockResolvedValueOnce(result1); + const response = await loggedInClient.post(`/edit`, params); + expect(response.status).toEqual(201); + }); + }); + }); + describe('save h5p content', () => { + describe('with valid request params', () => { + const setup = async () => { + const contentId = new ObjectId(0); + const id = '0000000'; + const metadata: IContentMetadata = { + embedTypes: [], + language: 'de', + mainLibrary: 'mainLib', + preloadedDependencies: [], + defaultLanguage: '', + license: '', + title: '123', + }; + const params: PostH5PContentCreateParams = { + parentType: H5PContentParentType.Lesson, + parentId: new ObjectId().toString(), + params: { + params: undefined, + metadata: { + embedTypes: [], + language: '', + mainLibrary: '', + preloadedDependencies: [], + defaultLanguage: '', + license: '', + title: '', + }, + }, + library: '123', + }; + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + const { studentAccount, studentUser } = createStudent(); + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + const loggedInClient = await testApiClient.login(studentAccount); + + return { contentId, id, metadata, loggedInClient, params }; + }; + it('should return 201 status', async () => { + const { contentId, id, metadata, loggedInClient, params } = await setup(); + const result1 = { id, metadata }; + h5PEditorUc.saveH5pContentGetMetadata.mockResolvedValueOnce(result1); + const response = await loggedInClient.post(`/edit/${contentId.toString()}`, params); + + expect(response.status).toEqual(201); + }); + }); + describe('with bad request params', () => { + const setup = async () => { + const notExistingContentId = new ObjectId(1); + const params: PostH5PContentCreateParams = { + parentType: H5PContentParentType.Lesson, + parentId: new ObjectId().toString(), + params: { + params: undefined, + metadata: { + embedTypes: [], + language: '', + mainLibrary: '', + preloadedDependencies: [], + defaultLanguage: '', + license: '', + title: '', + }, + }, + library: '123', + }; + const createStudent = () => UserAndAccountTestFactory.buildStudent(); + const { studentAccount, studentUser } = createStudent(); + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + const loggedInClient = await testApiClient.login(studentAccount); + + return { notExistingContentId, loggedInClient, params }; + }; + it('should return 500 status', async () => { + const { notExistingContentId, loggedInClient, params } = await setup(); + h5PEditorUc.saveH5pContentGetMetadata.mockRejectedValueOnce(new Error('Could not save H5P content')); + const response = await loggedInClient.post(`/edit/${notExistingContentId.toString()}`, params); + + expect(response.status).toEqual(500); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts deleted file mode 100644 index 57a8a66b347..00000000000 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { EntityManager } from '@mikro-orm/core'; -import { HttpStatus, INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; -import { H5PEditorTestModule } from '@modules/h5p-editor/h5p-editor-test.module'; - -describe('H5PEditor Controller (api)', () => { - let app: INestApplication; - let em: EntityManager; - let testApiClient: TestApiClient; - - beforeAll(async () => { - const module = await Test.createTestingModule({ - imports: [H5PEditorTestModule], - }).compile(); - - app = module.createNestApplication(); - await app.init(); - em = app.get(EntityManager); - testApiClient = new TestApiClient(app, 'h5p-editor'); - }); - - afterAll(async () => { - await app.close(); - }); - - describe('get player', () => { - describe('when user not exists', () => { - it('should respond with unauthorized exception', async () => { - const response = await testApiClient.get('dummyID/play'); - - expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); - expect(response.body).toEqual({ - type: 'UNAUTHORIZED', - title: 'Unauthorized', - message: 'Unauthorized', - code: 401, - }); - }); - }); - - describe('when user is allowed to view player', () => { - const createStudent = () => UserAndAccountTestFactory.buildStudent(); - - const setup = async () => { - const { studentAccount, studentUser } = createStudent(); - - await em.persistAndFlush([studentAccount, studentUser]); - em.clear(); - - const loggedInClient = await testApiClient.login(studentAccount); - - return { loggedInClient }; - }; - - it('should return the player', async () => { - const { loggedInClient } = await setup(); - - const response = await loggedInClient.get('dummyID/play'); - - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.text).toContain('

H5P Player Dummy

'); - }); - }); - }); - - describe('get editor', () => { - describe('when user not exists', () => { - it('should respond with unauthorized exception', async () => { - const response = await testApiClient.get('dummyID/edit'); - - expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); - expect(response.body).toEqual({ - type: 'UNAUTHORIZED', - title: 'Unauthorized', - message: 'Unauthorized', - code: 401, - }); - }); - }); - - describe('when user is allowed to view editor', () => { - const createStudent = () => UserAndAccountTestFactory.buildStudent(); - - const setup = async () => { - const { studentAccount, studentUser } = createStudent(); - - await em.persistAndFlush([studentAccount, studentUser]); - em.clear(); - - const loggedInClient = await testApiClient.login(studentAccount); - - return { loggedInClient }; - }; - - it('should return the editor', async () => { - const { loggedInClient } = await setup(); - - const response = await loggedInClient.get('dummyID/edit'); - - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.text).toContain('

H5P Editor Dummy

'); - }); - }); - }); -}); diff --git a/apps/server/src/modules/h5p-editor/controller/dto/ajax/get.params.ts b/apps/server/src/modules/h5p-editor/controller/dto/ajax/get.params.ts new file mode 100644 index 00000000000..50695c28b75 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/ajax/get.params.ts @@ -0,0 +1,23 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class AjaxGetQueryParams { + @IsString() + @IsNotEmpty() + action!: string; + + @IsString() + @IsOptional() + machineName?: string; + + @IsString() + @IsOptional() + majorVersion?: string; + + @IsString() + @IsOptional() + minorVersion?: string; + + @IsString() + @IsOptional() + language?: string; +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/ajax/index.ts b/apps/server/src/modules/h5p-editor/controller/dto/ajax/index.ts new file mode 100644 index 00000000000..3410511d0cc --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/ajax/index.ts @@ -0,0 +1,3 @@ +export * from './get.params'; +export * from './post.body.params'; +export * from './post.params'; diff --git a/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.spec.ts b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.spec.ts new file mode 100644 index 00000000000..8db32320969 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.spec.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { ValidationPipe } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + AjaxPostBodyParams, + ContentBodyParams, + LibrariesBodyParams, + LibraryParametersBodyParams, +} from './post.body.params'; +import { AjaxPostBodyParamsTransformPipe } from './post.body.params.transform-pipe'; + +jest.mock('@nestjs/common', () => { + return { + ...jest.requireActual('@nestjs/common'), + ValidationPipe: jest.fn().mockImplementation(() => { + return { + transform: jest.fn(), + createExceptionFactory: jest.fn(() => jest.fn(() => new Error('Mocked Error'))), + }; + }), + }; +}); + +describe('transform', () => { + let ajaxBodyTransformPipe: AjaxPostBodyParamsTransformPipe; + let emptyAjaxPostBodyParams1: AjaxPostBodyParams; + let emptyAjaxPostBodyParams2: AjaxPostBodyParams; + let emptyAjaxPostBodyParams3: AjaxPostBodyParams; + let emptyAjaxPostBodyParams4: AjaxPostBodyParams; + + let module: TestingModule; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let validationPipe: ValidationPipe; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [AjaxPostBodyParamsTransformPipe, ValidationPipe], + }).compile(); + validationPipe = module.get(ValidationPipe); + ajaxBodyTransformPipe = module.get(AjaxPostBodyParamsTransformPipe); + + const emptyLibrariesBodyParams: LibrariesBodyParams = { + libraries: [], + }; + + const emptyLibraryParametersBodyParams: LibraryParametersBodyParams = { + libraryParameters: '', + }; + + const emptyContentBodyParams: ContentBodyParams = { + contentId: '', + field: '', + }; + + emptyAjaxPostBodyParams1 = emptyLibrariesBodyParams; + emptyAjaxPostBodyParams2 = emptyContentBodyParams; + emptyAjaxPostBodyParams3 = emptyLibraryParametersBodyParams; + emptyAjaxPostBodyParams4 = undefined; + }); + + it('when libaries in value', async () => { + const result = await ajaxBodyTransformPipe.transform(emptyAjaxPostBodyParams1); + expect(result).toBeDefined(); + }); + + it('when contentId in value', async () => { + await expect(ajaxBodyTransformPipe.transform(emptyAjaxPostBodyParams2)).rejects.toThrowError('Mocked Error'); + }); + + it('when libaryParameters in value', async () => { + const result = await ajaxBodyTransformPipe.transform(emptyAjaxPostBodyParams3); + expect(result).toBeDefined(); + }); + + it('when not libaries | contentId | libaryParameters in value', async () => { + const result = await ajaxBodyTransformPipe.transform(emptyAjaxPostBodyParams4); + expect(result).toBeUndefined(); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.ts b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.ts new file mode 100644 index 00000000000..f4ab7c18e69 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.transform-pipe.ts @@ -0,0 +1,39 @@ +import { Injectable, PipeTransform, ValidationPipe } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; +import { + AjaxPostBodyParams, + LibrariesBodyParams, + ContentBodyParams, + LibraryParametersBodyParams, +} from './post.body.params'; + +/** + * This transform pipe allows nest to validate the incoming request. + * Since H5P does sent bodies with different shapes, this custom ValidationPipe makes sure the different cases are correctly validated. + */ + +@Injectable() +export class AjaxPostBodyParamsTransformPipe implements PipeTransform { + async transform(value: AjaxPostBodyParams): Promise { + if (value === undefined) { + return undefined; + } + if ('libraries' in value) { + value = plainToClass(LibrariesBodyParams, value); + } else if ('contentId' in value) { + value = plainToClass(ContentBodyParams, value); + } else if ('libraryParameters' in value) { + value = plainToClass(LibraryParametersBodyParams, value); + } + + const validationResult = await validate(value); + if (validationResult.length > 0) { + const validationPipe = new ValidationPipe(); + const exceptionFactory = validationPipe.createExceptionFactory(); + throw exceptionFactory(validationResult); + } + + return value; + } +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.ts b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.ts new file mode 100644 index 00000000000..496616810c6 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.body.params.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsMongoId, IsOptional, IsString } from 'class-validator'; + +export class LibrariesBodyParams { + @ApiProperty() + @IsArray() + @IsString({ each: true }) + libraries!: string[]; +} + +export class ContentBodyParams { + @ApiProperty() + @IsMongoId() + contentId!: string; + + @ApiProperty() + @IsString() + @IsOptional() + field!: string; +} + +export class LibraryParametersBodyParams { + @ApiProperty() + @IsString() + libraryParameters!: string; +} + +export type AjaxPostBodyParams = LibrariesBodyParams | ContentBodyParams | LibraryParametersBodyParams | undefined; diff --git a/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.params.ts b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.params.ts new file mode 100644 index 00000000000..b84dc984504 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/ajax/post.params.ts @@ -0,0 +1,27 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class AjaxPostQueryParams { + @IsString() + @IsNotEmpty() + action!: string; + + @IsString() + @IsOptional() + machineName?: string; + + @IsString() + @IsOptional() + majorVersion?: string; + + @IsString() + @IsOptional() + minorVersion?: string; + + @IsString() + @IsOptional() + language?: string; + + @IsString() + @IsOptional() + id?: string; +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/content-file.url.params.ts b/apps/server/src/modules/h5p-editor/controller/dto/content-file.url.params.ts new file mode 100644 index 00000000000..e8e4b404faa --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/content-file.url.params.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsMongoId, IsNotEmpty, IsString } from 'class-validator'; + +export class ContentFileUrlParams { + @ApiProperty() + @IsMongoId() + id!: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + filename!: string; +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor-response.spec.ts b/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor-response.spec.ts new file mode 100644 index 00000000000..451efaeca89 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor-response.spec.ts @@ -0,0 +1,26 @@ +import { IContentMetadata } from '@lumieducation/h5p-server/build/src/types'; +import { H5PContentMetadata } from './h5p-editor.response'; + +describe('H5PContentMetadata', () => { + let h5pContentMetadata: H5PContentMetadata; + + beforeEach(() => { + const testContentMetadata: IContentMetadata = { + embedTypes: ['iframe'], + language: 'en', + mainLibrary: 'testLibrary', + preloadedDependencies: [ + { machineName: 'Dependency1', majorVersion: 1, minorVersion: 0 }, + { machineName: 'Dependency2', majorVersion: 2, minorVersion: 0 }, + ], + defaultLanguage: '', + license: '', + title: '', + }; + h5pContentMetadata = new H5PContentMetadata(testContentMetadata); + }); + + it('should be defined', () => { + expect(h5pContentMetadata).toBeDefined(); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.params.ts b/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.params.ts new file mode 100644 index 00000000000..a8c6d8c466d --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.params.ts @@ -0,0 +1,82 @@ +import { IContentMetadata } from '@lumieducation/h5p-server'; +import { ApiProperty } from '@nestjs/swagger'; +import { SanitizeHtml } from '@shared/controller'; +import { EntityId, LanguageType } from '@shared/domain'; +import { IsEnum, IsMongoId, IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator'; +import { H5PContentParentType } from '../../entity'; + +export class GetH5PContentParams { + @ApiProperty({ enum: LanguageType, enumName: 'LanguageType' }) + @IsEnum(LanguageType) + @IsOptional() + language?: LanguageType; + + @ApiProperty() + @IsMongoId() + contentId!: string; +} + +export class GetH5PEditorParamsCreate { + @ApiProperty({ enum: LanguageType, enumName: 'LanguageType' }) + @IsEnum(LanguageType) + language!: LanguageType; +} + +export class GetH5PEditorParams { + @ApiProperty() + @IsMongoId() + contentId!: string; + + @ApiProperty({ enum: LanguageType, enumName: 'LanguageType' }) + @IsEnum(LanguageType) + language!: LanguageType; +} + +export class SaveH5PEditorParams { + @ApiProperty() + @IsMongoId() + contentId!: string; +} + +export class PostH5PContentParams { + @ApiProperty() + @IsMongoId() + contentId!: string; + + @ApiProperty() + @IsNotEmpty() + params!: unknown; + + @ApiProperty() + @IsNotEmpty() + metadata!: IContentMetadata; + + @ApiProperty() + @IsString() + @SanitizeHtml() + @IsNotEmpty() + mainLibraryUbername!: string; +} + +export class PostH5PContentCreateParams { + @ApiProperty({ enum: H5PContentParentType, enumName: 'H5PContentParentType' }) + @IsEnum(H5PContentParentType) + parentType!: H5PContentParentType; + + @ApiProperty() + @IsMongoId() + parentId!: EntityId; + + @ApiProperty() + @IsNotEmpty() + @IsObject() + params!: { + params: unknown; + metadata: IContentMetadata; + }; + + @ApiProperty() + @IsString() + @IsNotEmpty() + library!: string; +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.response.ts b/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.response.ts new file mode 100644 index 00000000000..b76f247fc8e --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/h5p-editor.response.ts @@ -0,0 +1,94 @@ +import { ContentParameters, IContentMetadata, IEditorModel, IIntegration } from '@lumieducation/h5p-server'; +import { ApiProperty } from '@nestjs/swagger'; +import { Readable } from 'stream'; + +export class H5PEditorModelResponse { + constructor(editorModel: IEditorModel) { + this.integration = editorModel.integration; + this.scripts = editorModel.scripts; + this.styles = editorModel.styles; + } + + @ApiProperty() + integration: IIntegration; + + // This is a list of URLs that point to the Javascript files the H5P editor needs to load + @ApiProperty() + scripts: string[]; + + // This is a list of URLs that point to the CSS files the H5P editor needs to load + @ApiProperty() + styles: string[]; +} + +export interface GetH5PFileResponse { + data: Readable; + etag?: string; + contentType?: string; + contentLength?: number; + contentRange?: string; + name: string; +} + +interface H5PContentResponse { + h5p: IContentMetadata; + library: string; + params: { + metadata: IContentMetadata; + params: ContentParameters; + }; +} + +export class H5PEditorModelContentResponse extends H5PEditorModelResponse { + constructor(editorModel: IEditorModel, content: H5PContentResponse) { + super(editorModel); + + this.library = content.library; + this.metadata = content.params.metadata; + this.params = content.params.params; + } + + @ApiProperty() + library: string; + + @ApiProperty() + metadata: IContentMetadata; + + @ApiProperty() + params: unknown; +} + +export class H5PContentMetadata { + constructor(metadata: IContentMetadata) { + this.mainLibrary = metadata.mainLibrary; + this.title = metadata.title; + } + + @ApiProperty() + title: string; + + @ApiProperty() + mainLibrary: string; +} + +export class H5PSaveResponse { + constructor(id: string, metadata: IContentMetadata) { + this.contentId = id; + this.metadata = metadata; + } + + @ApiProperty() + contentId!: string; + + @ApiProperty({ type: H5PContentMetadata }) + metadata!: H5PContentMetadata; +} + +export interface GetFileResponse { + data: Readable; + etag?: string; + contentType?: string; + contentLength?: number; + contentRange?: string; + name: string; +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts b/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts new file mode 100644 index 00000000000..1ac4fc092e3 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts @@ -0,0 +1,32 @@ +import { Readable } from 'stream'; +import { File } from '@shared/infra/s3-client'; + +export class H5pFileDto implements File { + constructor(file: H5pFileDto) { + this.name = file.name; + this.data = file.data; + this.mimeType = file.mimeType; + } + + name: string; + + data: Readable; + + mimeType: string; +} + +export interface GetH5pFileResponse { + data: Readable; + etag?: string; + contentType?: string; + contentLength?: number; + contentRange?: string; + name: string; +} + +export interface GetLibraryFile { + data: Readable; + contentType: string; + contentLength: number; + contentRange?: { start: number; end: number }; +} diff --git a/apps/server/src/modules/h5p-editor/controller/dto/index.ts b/apps/server/src/modules/h5p-editor/controller/dto/index.ts new file mode 100644 index 00000000000..dab538acc70 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/index.ts @@ -0,0 +1,6 @@ +export * from './ajax'; +export * from './content-file.url.params'; +export * from './h5p-editor.params'; +export * from './library-file.url.params'; +export * from './h5p-file.dto'; +export * from './h5p-editor.response'; diff --git a/apps/server/src/modules/h5p-editor/controller/dto/library-file.url.params.ts b/apps/server/src/modules/h5p-editor/controller/dto/library-file.url.params.ts new file mode 100644 index 00000000000..40d036b6e1f --- /dev/null +++ b/apps/server/src/modules/h5p-editor/controller/dto/library-file.url.params.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class LibraryFileUrlParams { + @ApiProperty() + @IsString() + @IsNotEmpty() + ubername!: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + file!: string; +} diff --git a/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts b/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts index 519f96e75e1..8c80d6bc0e4 100644 --- a/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts +++ b/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts @@ -1,49 +1,58 @@ -import { BadRequestException, Controller, ForbiddenException, Get, InternalServerErrorException } from '@nestjs/common'; +import { + BadRequestException, + Body, + Controller, + ForbiddenException, + Get, + HttpStatus, + InternalServerErrorException, + Param, + Post, + Query, + Req, + Res, + StreamableFile, + UploadedFiles, + UseInterceptors, +} from '@nestjs/common'; +import { FileFieldsInterceptor } from '@nestjs/platform-express'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; +import { ICurrentUser, CurrentUser } from '@modules/authentication'; +import { Request, Response } from 'express'; import { Authenticate } from '@modules/authentication/decorator/auth.decorator'; -// Dummy html response so we can test i-frame integration -const dummyResponse = (title: string) => ` - - - - - - - ${title} - - -

${title}

-

This response can be used for testing

- - -`; +import { H5PEditorUc } from '../uc/h5p.uc'; + +import { AjaxPostBodyParamsTransformPipe } from './dto/ajax/post.body.params.transform-pipe'; +import { + AjaxGetQueryParams, + AjaxPostBodyParams, + AjaxPostQueryParams, + ContentFileUrlParams, + GetH5PContentParams, + GetH5PEditorParams, + GetH5PEditorParamsCreate, + LibraryFileUrlParams, + PostH5PContentCreateParams, + SaveH5PEditorParams, +} from './dto'; +import { H5PEditorModelContentResponse, H5PEditorModelResponse, H5PSaveResponse } from './dto/h5p-editor.response'; @ApiTags('h5p-editor') @Authenticate('jwt') @Controller('h5p-editor') export class H5PEditorController { - @ApiOperation({ summary: 'Return dummy HTML for testing' }) - @ApiResponse({ status: 400, type: ApiValidationError }) - @ApiResponse({ status: 400, type: BadRequestException }) - @ApiResponse({ status: 403, type: ForbiddenException }) - @ApiResponse({ status: 500, type: InternalServerErrorException }) - @Get('/:contentId/play') - async getPlayer() { - // Dummy Response - return Promise.resolve(dummyResponse('H5P Player Dummy')); - } + constructor(private h5pEditorUc: H5PEditorUc) {} @ApiOperation({ summary: 'Return dummy HTML for testing' }) @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 400, type: BadRequestException }) @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) - @Get('/:contentId/edit') - async getEditor() { - // Dummy Response - return Promise.resolve(dummyResponse('H5P Editor Dummy')); + @Get('/play/:contentId') + async getPlayer(@CurrentUser() currentUser: ICurrentUser, @Param() params: GetH5PContentParams) { + return this.h5pEditorUc.getH5pPlayer(currentUser, params.contentId); } // Other Endpoints (incomplete list), paths not final @@ -53,4 +62,173 @@ export class H5PEditorController { // - ajax endpoint for h5p (e.g. GET/POST `/ajax/*`) // - static files from h5p-core (e.g. GET `/core/*`) // - static files for editor (e.g. GET `/editor/*`) + + @Get('libraries/:ubername/:file(*)') + async getLibraryFile(@Param() params: LibraryFileUrlParams, @Req() req: Request) { + const { data, contentType, contentLength } = await this.h5pEditorUc.getLibraryFile(params.ubername, params.file); + + req.on('close', () => data.destroy()); + + return new StreamableFile(data, { type: contentType, length: contentLength }); + } + + @Get('params/:id') + async getContentParameters(@Param('id') id: string, @CurrentUser() currentUser: ICurrentUser) { + const content = await this.h5pEditorUc.getContentParameters(id, currentUser); + + return content; + } + + @Get('content/:id/:filename(*)') + async getContentFile( + @Param() params: ContentFileUrlParams, + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + @CurrentUser() currentUser: ICurrentUser + ) { + const { data, contentType, contentLength, contentRange } = await this.h5pEditorUc.getContentFile( + params.id, + params.filename, + req, + currentUser + ); + + H5PEditorController.setRangeResponseHeaders(res, contentLength, contentRange); + + req.on('close', () => data.destroy()); + + return new StreamableFile(data, { type: contentType, length: contentLength }); + } + + @Get('temp-files/:file(*)') + async getTemporaryFile( + @CurrentUser() currentUser: ICurrentUser, + @Param('file') file: string, + @Req() req: Request, + @Res({ passthrough: true }) res: Response + ) { + const { data, contentType, contentLength, contentRange } = await this.h5pEditorUc.getTemporaryFile( + file, + req, + currentUser + ); + + H5PEditorController.setRangeResponseHeaders(res, contentLength, contentRange); + + req.on('close', () => data.destroy()); + + return new StreamableFile(data, { type: contentType, length: contentLength }); + } + + @Get('ajax') + async getAjax(@Query() query: AjaxGetQueryParams, @CurrentUser() currentUser: ICurrentUser) { + const response = this.h5pEditorUc.getAjax(query, currentUser); + + return response; + } + + @Post('ajax') + @UseInterceptors( + FileFieldsInterceptor([ + { name: 'file', maxCount: 1 }, + { name: 'h5p', maxCount: 1 }, + ]) + ) + async postAjax( + @Body(AjaxPostBodyParamsTransformPipe) body: AjaxPostBodyParams, + @Query() query: AjaxPostQueryParams, + @CurrentUser() currentUser: ICurrentUser, + @UploadedFiles() files?: { file?: Express.Multer.File[]; h5p?: Express.Multer.File[] } + ) { + const contentFile = files?.file?.[0]; + const h5pFile = files?.h5p?.[0]; + + const result = await this.h5pEditorUc.postAjax(currentUser, query, body, contentFile, h5pFile); + + return result; + } + + @Post('/delete/:contentId') + async deleteH5pContent( + @Param() params: GetH5PContentParams, + @CurrentUser() currentUser: ICurrentUser + ): Promise { + const deleteSuccessfull = this.h5pEditorUc.deleteH5pContent(currentUser, params.contentId); + + return deleteSuccessfull; + } + + @Get('/edit/:language') + @ApiResponse({ status: 200, type: H5PEditorModelResponse }) + async getNewH5PEditor(@Param() params: GetH5PEditorParamsCreate, @CurrentUser() currentUser: ICurrentUser) { + const editorModel = await this.h5pEditorUc.getEmptyH5pEditor(currentUser, params.language); + + return new H5PEditorModelResponse(editorModel); + } + + @Get('/edit/:contentId/:language') + @ApiResponse({ status: 200, type: H5PEditorModelContentResponse }) + async getH5PEditor(@Param() params: GetH5PEditorParams, @CurrentUser() currentUser: ICurrentUser) { + const { editorModel, content } = await this.h5pEditorUc.getH5pEditor( + currentUser, + params.contentId, + params.language + ); + + return new H5PEditorModelContentResponse(editorModel, content); + } + + @Post('/edit') + @ApiResponse({ status: 201, type: H5PSaveResponse }) + async createH5pContent(@Body() body: PostH5PContentCreateParams, @CurrentUser() currentUser: ICurrentUser) { + const response = await this.h5pEditorUc.createH5pContentGetMetadata( + currentUser, + body.params.params, + body.params.metadata, + body.library, + body.parentType, + body.parentId + ); + + const saveResponse = new H5PSaveResponse(response.id, response.metadata); + + return saveResponse; + } + + @Post('/edit/:contentId') + @ApiResponse({ status: 201, type: H5PSaveResponse }) + async saveH5pContent( + @Body() body: PostH5PContentCreateParams, + @Param() params: SaveH5PEditorParams, + @CurrentUser() currentUser: ICurrentUser + ) { + const response = await this.h5pEditorUc.saveH5pContentGetMetadata( + params.contentId, + currentUser, + body.params.params, + body.params.metadata, + body.library, + body.parentType, + body.parentId + ); + + const saveResponse = new H5PSaveResponse(response.id, response.metadata); + + return saveResponse; + } + + private static setRangeResponseHeaders(res: Response, contentLength: number, range?: { start: number; end: number }) { + if (range) { + const contentRangeHeader = `bytes ${range.start}-${range.end}/${contentLength}`; + + res.set({ + 'Accept-Ranges': 'bytes', + 'Content-Range': contentRangeHeader, + }); + + res.status(HttpStatus.PARTIAL_CONTENT); + } else { + res.status(HttpStatus.OK); + } + } } diff --git a/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts new file mode 100644 index 00000000000..cea707c8ccf --- /dev/null +++ b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts @@ -0,0 +1,42 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { ContentMetadata, H5PContent, H5PContentParentType, IH5PContentProperties } from './h5p-content.entity'; + +describe('H5PContent class', () => { + describe('when an H5PContent instance is created', () => { + const setup = () => { + const dummyIH5PContentProperties: IH5PContentProperties = { + creatorId: '507f1f77bcf86cd799439011', + parentType: H5PContentParentType.Lesson, + parentId: '507f1f77bcf86cd799439012', + schoolId: '507f1f77bcf86cd799439013', + metadata: new ContentMetadata({ + embedTypes: ['iframe'], + language: 'en', + mainLibrary: 'mainLibrary123', + defaultLanguage: 'en', + license: 'MIT', + title: 'Title Example', + preloadedDependencies: [], + dynamicDependencies: [], + editorDependencies: [], + }), + content: {}, + }; + + const h5pContent = new H5PContent(dummyIH5PContentProperties); + return { h5pContent, dummyIH5PContentProperties }; + }; + + it('should correctly return the creatorId', () => { + const { h5pContent, dummyIH5PContentProperties } = setup(); + const expectedCreatorId = new ObjectId(dummyIH5PContentProperties.creatorId).toHexString(); + expect(h5pContent.creatorId).toBe(expectedCreatorId); + }); + + it('should correctly return the schoolId', () => { + const { h5pContent, dummyIH5PContentProperties } = setup(); + const expectedSchoolId = new ObjectId(dummyIH5PContentProperties.schoolId).toHexString(); + expect(h5pContent.schoolId).toBe(expectedSchoolId); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts new file mode 100644 index 00000000000..3f9e6113172 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts @@ -0,0 +1,163 @@ +import { IContentMetadata, ILibraryName } from '@lumieducation/h5p-server'; +import { IContentAuthor, IContentChange } from '@lumieducation/h5p-server/build/src/types'; +import { Embeddable, Embedded, Entity, Enum, Index, JsonType, Property } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; + +@Embeddable() +export class ContentMetadata implements IContentMetadata { + @Property({ nullable: true }) + dynamicDependencies?: ILibraryName[]; + + @Property({ nullable: true }) + editorDependencies?: ILibraryName[]; + + @Property() + embedTypes: ('iframe' | 'div')[]; + + @Property({ nullable: true }) + h?: string; + + @Property() + language: string; + + @Property() + mainLibrary: string; + + @Property({ nullable: true }) + metaDescription?: string; + + @Property({ nullable: true }) + metaKeywords?: string; + + @Property() + preloadedDependencies: ILibraryName[]; + + @Property({ nullable: true }) + w?: string; + + @Property() + defaultLanguage: string; + + @Property({ nullable: true }) + a11yTitle?: string; + + @Property() + license: string; + + @Property({ nullable: true }) + licenseVersion?: string; + + @Property({ nullable: true }) + yearFrom?: string; + + @Property({ nullable: true }) + yearTo?: string; + + @Property({ nullable: true }) + source?: string; + + @Property() + title: string; + + @Property({ nullable: true }) + authors?: IContentAuthor[]; + + @Property({ nullable: true }) + licenseExtras?: string; + + @Property({ nullable: true }) + changes?: IContentChange[]; + + @Property({ nullable: true }) + authorComments?: string; + + @Property({ nullable: true }) + contentType?: string; + + constructor(metadata: IContentMetadata) { + this.embedTypes = metadata.embedTypes; + this.language = metadata.language; + this.mainLibrary = metadata.mainLibrary; + this.defaultLanguage = metadata.defaultLanguage; + this.license = metadata.license; + this.title = metadata.title; + this.preloadedDependencies = metadata.preloadedDependencies; + this.dynamicDependencies = metadata.dynamicDependencies; + this.editorDependencies = metadata.editorDependencies; + this.h = metadata.h; + this.metaDescription = metadata.metaDescription; + this.metaKeywords = metadata.metaKeywords; + this.w = metadata.w; + this.a11yTitle = metadata.a11yTitle; + this.licenseVersion = metadata.licenseVersion; + this.yearFrom = metadata.yearFrom; + this.yearTo = metadata.yearTo; + this.source = metadata.source; + this.authors = metadata.authors; + this.licenseExtras = metadata.licenseExtras; + this.changes = metadata.changes; + this.authorComments = metadata.authorComments; + this.contentType = metadata.contentType; + } +} + +export enum H5PContentParentType { + 'Lesson' = 'lessons', +} + +export interface IH5PContentProperties { + creatorId: EntityId; + parentType: H5PContentParentType; + parentId: EntityId; + schoolId: EntityId; + metadata: ContentMetadata; + content: unknown; +} + +@Entity({ tableName: 'h5p-editor-content' }) +export class H5PContent extends BaseEntityWithTimestamps { + @Property({ fieldName: 'creator' }) + _creatorId: ObjectId; + + get creatorId(): EntityId { + return this._creatorId.toHexString(); + } + + @Index() + @Enum() + parentType: H5PContentParentType; + + @Index() + @Property({ fieldName: 'parent' }) + _parentId: ObjectId; + + get parentId(): EntityId { + return this._parentId.toHexString(); + } + + @Property({ fieldName: 'school' }) + _schoolId: ObjectId; + + get schoolId(): EntityId { + return this._schoolId.toHexString(); + } + + @Embedded(() => ContentMetadata) + metadata: ContentMetadata; + + @Property({ type: JsonType }) + content: unknown; + + constructor({ parentType, parentId, creatorId, schoolId, metadata, content }: IH5PContentProperties) { + super(); + + this.parentType = parentType; + this._parentId = new ObjectId(parentId); + this._creatorId = new ObjectId(creatorId); + this._schoolId = new ObjectId(schoolId); + + this.metadata = metadata; + this.content = content; + } +} diff --git a/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts b/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts new file mode 100644 index 00000000000..a4ebeb30e8a --- /dev/null +++ b/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts @@ -0,0 +1,41 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { ITemporaryFile, IFileStats } from '@lumieducation/h5p-server'; +import { BaseEntityWithTimestamps } from '@shared/domain'; + +export interface ITemporaryFileProperties { + filename: string; + ownedByUserId: string; + expiresAt: Date; + birthtime: Date; + size: number; +} + +@Entity({ tableName: 'h5p-editor-temp-file' }) +export class H5pEditorTempFile extends BaseEntityWithTimestamps implements ITemporaryFile, IFileStats { + /** + * The name by which the file can be identified; can be a path including subdirectories (e.g. 'images/xyz.png') + */ + @Property() + filename: string; + + @Property() + expiresAt: Date; + + @Property() + ownedByUserId: string; + + @Property() + birthtime: Date; + + @Property() + size: number; + + constructor({ filename, ownedByUserId, expiresAt, birthtime, size }: ITemporaryFileProperties) { + super(); + this.filename = filename; + this.ownedByUserId = ownedByUserId; + this.expiresAt = expiresAt; + this.birthtime = birthtime; + this.size = size; + } +} diff --git a/apps/server/src/modules/h5p-editor/entity/index.ts b/apps/server/src/modules/h5p-editor/entity/index.ts new file mode 100644 index 00000000000..e95c0f12c94 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/entity/index.ts @@ -0,0 +1,3 @@ +export * from './h5p-content.entity'; +export * from './library.entity'; +export * from './h5p-editor-tempfile.entity'; diff --git a/apps/server/src/modules/h5p-editor/entity/library.entity.spec.ts b/apps/server/src/modules/h5p-editor/entity/library.entity.spec.ts new file mode 100644 index 00000000000..7a917398411 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/entity/library.entity.spec.ts @@ -0,0 +1,223 @@ +import { ILibraryMetadata } from '@lumieducation/h5p-server'; +import { FileMetadata, InstalledLibrary, LibraryName, Path } from './library.entity'; + +describe('InstalledLibrary', () => { + let addonLibVersionOne: InstalledLibrary; + let addonLibVersionOneMinorChange: InstalledLibrary; + let addonLibVersionOnePatchChange: InstalledLibrary; + let addonLibVersionTwo: InstalledLibrary; + + beforeAll(() => { + const testingLibMetadataVersionOne: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'testing', + majorVersion: 1, + minorVersion: 2, + }; + const testingLibVersionOne = new InstalledLibrary(testingLibMetadataVersionOne); + testingLibVersionOne.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadataVersionOne: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'addonVersionOne', + majorVersion: 1, + minorVersion: 2, + }; + addonLibVersionOne = new InstalledLibrary(addonLibMetadataVersionOne); + addonLibVersionOne.addTo = { player: { machineNames: [testingLibVersionOne.machineName] } }; + + const testingLibMetadataVersionOneMinorChange: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'testing', + majorVersion: 1, + minorVersion: 5, + }; + const testingLibVersionOneMinorChange = new InstalledLibrary(testingLibMetadataVersionOneMinorChange); + testingLibVersionOne.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadataVersionOneMinorChange: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'addonVersionOne', + majorVersion: 1, + minorVersion: 5, + }; + addonLibVersionOneMinorChange = new InstalledLibrary(addonLibMetadataVersionOneMinorChange); + addonLibVersionOneMinorChange.addTo = { player: { machineNames: [testingLibVersionOneMinorChange.machineName] } }; + + const testingLibMetadataVersionOnePatchChange: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 5, + machineName: 'testing', + majorVersion: 1, + minorVersion: 2, + }; + const testingLibVersionOnePatchChange = new InstalledLibrary(testingLibMetadataVersionOnePatchChange); + testingLibVersionOne.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadataVersionOnePatchChange: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 5, + machineName: 'addonVersionOne', + majorVersion: 1, + minorVersion: 2, + }; + addonLibVersionOnePatchChange = new InstalledLibrary(addonLibMetadataVersionOnePatchChange); + addonLibVersionOnePatchChange.addTo = { player: { machineNames: [testingLibVersionOnePatchChange.machineName] } }; + + const testingLibMetadataVersionTwo: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 4, + machineName: 'addonVersionTwo', + majorVersion: 2, + minorVersion: 3, + }; + const testingLibVersionTwo = new InstalledLibrary(testingLibMetadataVersionTwo); + testingLibVersionTwo.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadataVersionTwo: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 4, + machineName: 'addonVersionTwo', + majorVersion: 2, + minorVersion: 3, + }; + addonLibVersionTwo = new InstalledLibrary(addonLibMetadataVersionTwo); + addonLibVersionTwo.addTo = { player: { machineNames: [testingLibVersionTwo.machineName] } }; + }); + + describe('simple_compare', () => { + it('should return 1 if a is greater than b', () => { + expect(InstalledLibrary.simple_compare(5, 3)).toBe(1); + }); + + it('should return -1 if a is less than b', () => { + expect(InstalledLibrary.simple_compare(3, 5)).toBe(-1); + }); + + it('should return 0 if a is equal to b', () => { + expect(InstalledLibrary.simple_compare(3, 3)).toBe(0); + }); + }); + + describe('compare', () => { + describe('when compare', () => {}); + it('should return -1', () => { + const result = addonLibVersionOne.compare(addonLibVersionTwo); + expect(result).toBe(-1); + }); + describe('when compare library Version', () => { + it('should call compareVersions', () => { + const compareVersionsSpy = ( + jest.spyOn(addonLibVersionOne, 'compareVersions') as jest.SpyInstance + ).mockReturnValueOnce(0); + addonLibVersionOne.compare(addonLibVersionOne); + expect(compareVersionsSpy).toHaveBeenCalled(); + compareVersionsSpy.mockRestore(); + }); + }); + }); + + describe('compareVersions', () => { + describe('when calling compareVersions with Major Change', () => { + it('should return -1 and call simple_compare once', () => { + const simpleCompareSpy = jest.spyOn(InstalledLibrary, 'simple_compare'); + const result = addonLibVersionOne.compareVersions(addonLibVersionTwo); + expect(result).toBe(-1); + expect(simpleCompareSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('when calling compareVersions with Minor Change', () => { + it('should return -1 and call simple_compare three times', () => { + const simpleCompareSpy = jest.spyOn(InstalledLibrary, 'simple_compare'); + const result = addonLibVersionOne.compareVersions(addonLibVersionOneMinorChange); + expect(result).toBe(-1); + expect(simpleCompareSpy).toHaveBeenCalledTimes(3); + }); + }); + + describe('when calling compareVersions with same Major & Minor Versions', () => { + it('should return call simple_compare with patch versions', () => { + const simpleCompareSpy = jest.spyOn(InstalledLibrary, 'simple_compare'); + const result = addonLibVersionOne.compareVersions(addonLibVersionOnePatchChange); + expect(result).toBe(-1); + expect(simpleCompareSpy).toHaveBeenCalledWith( + addonLibVersionOne.patchVersion, + addonLibVersionOnePatchChange.patchVersion + ); + }); + }); + }); +}); + +describe('LibraryName', () => { + let libraryName: LibraryName; + + beforeEach(() => { + libraryName = new LibraryName('test', 1, 2); + }); + + it('should be defined', () => { + expect(libraryName).toBeDefined(); + }); + + it('should create libraryName', () => { + const newlibraryName = new LibraryName('newtest', 1, 2); + expect(newlibraryName.machineName).toEqual('newtest'); + }); + + it('should change libraryName', () => { + libraryName.machineName = 'changed-name'; + expect(libraryName.machineName).toEqual('changed-name'); + }); +}); + +describe('Path', () => { + let path: Path; + + beforeEach(() => { + path = new Path(''); + }); + + it('should be defined', () => { + expect(path).toBeDefined(); + }); + + it('should create path', () => { + const newPath = new Path('test-path'); + expect(newPath.path).toEqual('test-path'); + }); + + it('should change path', () => { + path.path = 'new-path'; + expect(path.path).toEqual('new-path'); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/entity/library.entity.ts b/apps/server/src/modules/h5p-editor/entity/library.entity.ts new file mode 100644 index 00000000000..868397f7266 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/entity/library.entity.ts @@ -0,0 +1,249 @@ +import { IInstalledLibrary, ILibraryName } from '@lumieducation/h5p-server'; +import { IFileStats, ILibraryMetadata, IPath } from '@lumieducation/h5p-server/build/src/types'; +import { Entity, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain'; + +export class Path implements IPath { + @Property() + path: string; + + constructor(path: string) { + this.path = path; + } +} + +export class LibraryName implements ILibraryName { + @Property() + machineName: string; + + @Property() + majorVersion: number; + + @Property() + minorVersion: number; + + constructor(machineName: string, majorVersion: number, minorVersion: number) { + this.machineName = machineName; + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + } +} + +export class FileMetadata implements IFileStats { + name: string; + + birthtime: Date; + + size: number; + + constructor(name: string, birthtime: Date, size: number) { + this.name = name; + this.birthtime = birthtime; + this.size = size; + } +} + +@Entity({ tableName: 'h5p_library' }) +export class InstalledLibrary extends BaseEntityWithTimestamps implements IInstalledLibrary { + @Property() + machineName: string; + + @Property() + majorVersion: number; + + @Property() + minorVersion: number; + + @Property() + patchVersion: number; + + /** + * Addons can be added to other content types by + */ + @Property({ nullable: true }) + addTo?: { + content?: { + types?: { + text?: { + /** + * If any string property in the parameters matches the regex, + * the addon will be activated for the content. + */ + regex?: string; + }; + }[]; + }; + /** + * Contains cases in which the library should be added to the editor. + * + * This is an extension to the H5P library metadata structure made by + * h5p-nodejs-library. That way addons can specify to which editors + * they should be added in general. The PHP implementation hard-codes + * this list into the server, which we want to avoid here. + */ + editor?: { + /** + * A list of machine names in which the addon should be added. + */ + machineNames: string[]; + }; + /** + * Contains cases in which the library should be added to the player. + * + * This is an extension to the H5P library metadata structure made by + * h5p-nodejs-library. That way addons can specify to which editors + * they should be added in general. The PHP implementation hard-codes + * this list into the server, which we want to avoid here. + */ + player?: { + /** + * A list of machine names in which the addon should be added. + */ + machineNames: string[]; + }; + }; + + /** + * If set to true, the library can only be used be users who have this special + * privilege. + */ + @Property() + restricted: boolean; + + @Property({ nullable: true }) + author?: string; + + /** + * The core API required to run the library. + */ + @Property({ nullable: true }) + coreApi?: { + majorVersion: number; + minorVersion: number; + }; + + @Property({ nullable: true }) + description?: string; + + @Property({ nullable: true }) + dropLibraryCss?: { + machineName: string; + }[]; + + @Property({ nullable: true }) + dynamicDependencies?: LibraryName[]; + + @Property({ nullable: true }) + editorDependencies?: LibraryName[]; + + @Property({ nullable: true }) + embedTypes?: ('iframe' | 'div')[]; + + @Property({ nullable: true }) + fullscreen?: 0 | 1; + + @Property({ nullable: true }) + h?: number; + + @Property({ nullable: true }) + license?: string; + + @Property({ nullable: true }) + metadataSettings?: { + disable: 0 | 1; + disableExtraTitleField: 0 | 1; + }; + + @Property({ nullable: true }) + preloadedCss?: Path[]; + + @Property({ nullable: true }) + preloadedDependencies?: LibraryName[]; + + @Property({ nullable: true }) + preloadedJs?: Path[]; + + @Property() + runnable: boolean | 0 | 1; + + @Property() + title: string; + + @Property({ nullable: true }) + w?: number; + + @Property({ nullable: true }) + requiredExtensions?: { + sharedState: number; + }; + + @Property({ nullable: true }) + state?: { + snapshotSchema: boolean; + opSchema: boolean; + snapshotLogicChecks: boolean; + opLogicChecks: boolean; + }; + + @Property() + files: FileMetadata[]; + + public static simple_compare(a: number, b: number): number { + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; + } + + public compare(otherLibrary: IInstalledLibrary): number { + if (this.machineName === otherLibrary.machineName) { + return this.compareVersions(otherLibrary); + } + return this.machineName > otherLibrary.machineName ? 1 : -1; + } + + public compareVersions(otherLibrary: ILibraryName & { patchVersion?: number }): number { + let result = InstalledLibrary.simple_compare(this.majorVersion, otherLibrary.majorVersion); + if (result !== 0) { + return result; + } + result = InstalledLibrary.simple_compare(this.minorVersion, otherLibrary.minorVersion); + if (result !== 0) { + return result; + } + return InstalledLibrary.simple_compare(this.patchVersion, otherLibrary.patchVersion as number); + } + + constructor(libraryMetadata: ILibraryMetadata, restricted = false, files: FileMetadata[] = []) { + super(); + this.machineName = libraryMetadata.machineName; + this.majorVersion = libraryMetadata.majorVersion; + this.minorVersion = libraryMetadata.minorVersion; + this.patchVersion = libraryMetadata.patchVersion; + this.runnable = libraryMetadata.runnable; + this.title = libraryMetadata.title; + this.addTo = libraryMetadata.addTo; + this.author = libraryMetadata.author; + this.coreApi = libraryMetadata.coreApi; + this.description = libraryMetadata.description; + this.dropLibraryCss = libraryMetadata.dropLibraryCss; + this.dynamicDependencies = libraryMetadata.dynamicDependencies; + this.editorDependencies = libraryMetadata.editorDependencies; + this.embedTypes = libraryMetadata.embedTypes; + this.fullscreen = libraryMetadata.fullscreen; + this.h = libraryMetadata.h; + this.license = libraryMetadata.license; + this.metadataSettings = libraryMetadata.metadataSettings; + this.preloadedCss = libraryMetadata.preloadedCss; + this.preloadedDependencies = libraryMetadata.preloadedDependencies; + this.preloadedJs = libraryMetadata.preloadedJs; + this.w = libraryMetadata.w; + this.requiredExtensions = libraryMetadata.requiredExtensions; + this.state = libraryMetadata.state; + this.restricted = restricted; + this.files = files; + } +} diff --git a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts index fccb5e2841b..62cd06dcf6e 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts @@ -1,27 +1,49 @@ import { DynamicModule, Module } from '@nestjs/common'; -import { Account, Role, SchoolEntity, SchoolYearEntity, User } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory-database/types'; +import { ALL_ENTITIES } from '@shared/domain'; +import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@shared/infra/database'; import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; +import { S3ClientModule } from '@shared/infra/s3-client'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '@modules/authentication/authentication.module'; -import { AuthorizationModule } from '@modules/authorization'; +import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; +import { UserModule } from '@modules/user'; import { AuthenticationApiModule } from '../authentication/authentication-api.module'; import { H5PEditorModule } from './h5p-editor.module'; +import { H5PContentRepo, LibraryRepo, TemporaryFileRepo } from './repo'; +import { ContentStorage, LibraryStorage, TemporaryFileStorage } from './service'; +import { H5PEditorUc } from './uc/h5p.uc'; +import { s3ConfigContent, s3ConfigLibraries } from './h5p-editor.config'; +import { H5PEditorController } from './controller'; +import { H5PEditorProvider, H5PAjaxEndpointProvider, H5PPlayerProvider } from './provider'; +import { H5PContent } from './entity'; const imports = [ H5PEditorModule, - MongoMemoryDatabaseModule.forRoot({ entities: [Account, Role, SchoolEntity, SchoolYearEntity, User] }), + MongoMemoryDatabaseModule.forRoot({ entities: [...ALL_ENTITIES, H5PContent] }), AuthenticationApiModule, - AuthorizationModule, + AuthorizationReferenceModule, AuthenticationModule, + UserModule, CoreModule, LoggerModule, RabbitMQWrapperTestModule, + S3ClientModule.register([s3ConfigContent, s3ConfigLibraries]), ]; -const controllers = []; -const providers = []; +const controllers = [H5PEditorController]; +const providers = [ + H5PEditorUc, + H5PPlayerProvider, + H5PEditorProvider, + H5PAjaxEndpointProvider, + H5PContentRepo, + LibraryRepo, + TemporaryFileRepo, + ContentStorage, + LibraryStorage, + TemporaryFileStorage, +]; + @Module({ imports, controllers, diff --git a/apps/server/src/modules/h5p-editor/h5p-editor.config.ts b/apps/server/src/modules/h5p-editor/h5p-editor.config.ts index a5b667897b3..f02084aa4e5 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor.config.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor.config.ts @@ -1,8 +1,34 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { S3Config } from '@shared/infra/s3-client'; const h5pEditorConfig = { NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, INCOMING_REQUEST_TIMEOUT: Configuration.get('INCOMING_REQUEST_TIMEOUT_API') as number, }; +export const translatorConfig = { + AVAILABLE_LANGUAGES: (Configuration.get('I18N__AVAILABLE_LANGUAGES') as string).split(','), +}; + +export const H5P_CONTENT_S3_CONNECTION = 'H5P_CONTENT_S3_CONNECTION'; +export const H5P_LIBRARIES_S3_CONNECTION = 'H5P_LIBRARIES_S3_CONNECTION'; + +export const s3ConfigContent: S3Config = { + connectionName: H5P_CONTENT_S3_CONNECTION, + endpoint: Configuration.get('H5P_EDITOR__S3_ENDPOINT') as string, + region: Configuration.get('H5P_EDITOR__S3_REGION') as string, + bucket: Configuration.get('H5P_EDITOR__S3_BUCKET_CONTENT') as string, + accessKeyId: Configuration.get('H5P_EDITOR__S3_ACCESS_KEY_ID_RW') as string, + secretAccessKey: Configuration.get('H5P_EDITOR__S3_SECRET_ACCESS_KEY_RW') as string, +}; + +export const s3ConfigLibraries: S3Config = { + connectionName: H5P_LIBRARIES_S3_CONNECTION, + endpoint: Configuration.get('H5P_EDITOR__S3_ENDPOINT') as string, + region: Configuration.get('H5P_EDITOR__S3_REGION') as string, + bucket: Configuration.get('H5P_EDITOR__S3_BUCKET_LIBRARIES') as string, + accessKeyId: Configuration.get('H5P_EDITOR__S3_ACCESS_KEY_ID_R') as string, + secretAccessKey: Configuration.get('H5P_EDITOR__S3_SECRET_ACCESS_KEY_R') as string, +}; + export const config = () => h5pEditorConfig; diff --git a/apps/server/src/modules/h5p-editor/h5p-editor.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor.module.ts index 442f0a04409..34efcedd69e 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor.module.ts @@ -2,14 +2,22 @@ import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { Account, Role, SchoolEntity, SchoolYearEntity, SystemEntity, User } from '@shared/domain'; +import { ALL_ENTITIES } from '@shared/domain'; +import { RabbitMQWrapperModule } from '@shared/infra/rabbitmq'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { Logger } from '@src/core/logger'; -import { AuthorizationModule } from '@modules/authorization'; +import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; +import { UserModule } from '@modules/user'; +import { S3ClientModule } from '@shared/infra/s3-client'; import { AuthenticationModule } from '../authentication/authentication.module'; import { H5PEditorController } from './controller/h5p-editor.controller'; -import { config } from './h5p-editor.config'; +import { H5PContent, InstalledLibrary, H5pEditorTempFile } from './entity'; +import { config, s3ConfigContent, s3ConfigLibraries } from './h5p-editor.config'; +import { H5PContentRepo, LibraryRepo, TemporaryFileRepo } from './repo'; +import { ContentStorage, LibraryStorage, TemporaryFileStorage } from './service'; +import { H5PEditorProvider, H5PAjaxEndpointProvider, H5PPlayerProvider } from './provider'; +import { H5PEditorUc } from './uc/h5p.uc'; const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { findOneOrFailHandler: (entityName: string, where: Dictionary | IPrimaryKey) => @@ -19,8 +27,10 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { const imports = [ AuthenticationModule, - AuthorizationModule, + AuthorizationReferenceModule, CoreModule, + UserModule, + RabbitMQWrapperModule, MikroOrmModule.forRoot({ ...defaultMikroOrmOptions, type: 'mongo', @@ -28,16 +38,28 @@ const imports = [ clientUrl: DB_URL, password: DB_PASSWORD, user: DB_USERNAME, - entities: [User, Account, Role, SchoolEntity, SystemEntity, SchoolYearEntity], - - // debug: true, // use it for locally debugging of querys + // Needs ALL_ENTITIES for authorization + entities: [...ALL_ENTITIES, H5PContent, H5pEditorTempFile, InstalledLibrary], }), ConfigModule.forRoot(createConfigModuleOptions(config)), + S3ClientModule.register([s3ConfigContent, s3ConfigLibraries]), ]; const controllers = [H5PEditorController]; -const providers = [Logger]; +const providers = [ + Logger, + H5PEditorUc, + H5PContentRepo, + LibraryRepo, + TemporaryFileRepo, + H5PEditorProvider, + H5PPlayerProvider, + H5PAjaxEndpointProvider, + ContentStorage, + LibraryStorage, + TemporaryFileStorage, +]; @Module({ imports, diff --git a/apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.spec.ts b/apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.spec.ts new file mode 100644 index 00000000000..164c19dc269 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.spec.ts @@ -0,0 +1,20 @@ +import { AuthorizableReferenceType } from '@modules/authorization/domain'; +import { NotImplementedException } from '@nestjs/common'; +import { H5PContentParentType } from '../entity'; +import { H5PContentMapper } from './h5p-content.mapper'; + +describe('H5PContentMapper', () => { + describe('mapToAllowedAuthorizationEntityType()', () => { + it('should return allowed type equal Course', () => { + const result = H5PContentMapper.mapToAllowedAuthorizationEntityType(H5PContentParentType.Lesson); + expect(result).toBe(AuthorizableReferenceType.Lesson); + }); + + it('should throw NotImplementedException', () => { + const exec = () => { + H5PContentMapper.mapToAllowedAuthorizationEntityType('' as H5PContentParentType); + }; + expect(exec).toThrowError(NotImplementedException); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.ts b/apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.ts new file mode 100644 index 00000000000..1b760036db6 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/mapper/h5p-content.mapper.ts @@ -0,0 +1,19 @@ +import { NotImplementedException } from '@nestjs/common'; +import { AuthorizableReferenceType } from '@src/modules/authorization/domain'; +import { H5PContentParentType } from '../entity'; + +export class H5PContentMapper { + static mapToAllowedAuthorizationEntityType(type: H5PContentParentType): AuthorizableReferenceType { + const types = new Map(); + + types.set(H5PContentParentType.Lesson, AuthorizableReferenceType.Lesson); + + const res = types.get(type); + + if (!res) { + throw new NotImplementedException(); + } + + return res; + } +} diff --git a/apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.spec.ts b/apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.spec.ts new file mode 100644 index 00000000000..ad5d5332cc0 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.spec.ts @@ -0,0 +1,26 @@ +import { H5pError } from '@lumieducation/h5p-server'; +import { HttpException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { H5PErrorMapper } from './h5p-error.mapper'; + +describe('H5PErrorMapper', () => { + let h5pErrorMapper: H5PErrorMapper; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + providers: [H5PErrorMapper], + }).compile(); + + h5pErrorMapper = app.get(H5PErrorMapper); + }); + + describe('mapH5pError', () => { + it('should map H5pError to HttpException', () => { + const error = new H5pError('h5p error massage'); + const result = h5pErrorMapper.mapH5pError(error); + + expect(result).toBeInstanceOf(HttpException); + expect(result.message).toEqual('h5p error massage'); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.ts b/apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.ts new file mode 100644 index 00000000000..1cd69875985 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/mapper/h5p-error.mapper.ts @@ -0,0 +1,8 @@ +import { H5pError } from '@lumieducation/h5p-server'; +import { HttpException } from '@nestjs/common'; + +export class H5PErrorMapper { + public mapH5pError(error: H5pError) { + return new HttpException(error.message, error.httpStatusCode); + } +} diff --git a/apps/server/src/modules/h5p-editor/provider/h5p-ajax-endpoint.provider.ts b/apps/server/src/modules/h5p-editor/provider/h5p-ajax-endpoint.provider.ts new file mode 100644 index 00000000000..b5bc43d291c --- /dev/null +++ b/apps/server/src/modules/h5p-editor/provider/h5p-ajax-endpoint.provider.ts @@ -0,0 +1,11 @@ +import { H5PAjaxEndpoint, H5PEditor } from '@lumieducation/h5p-server'; + +export const H5PAjaxEndpointProvider = { + provide: H5PAjaxEndpoint, + inject: [H5PEditor], + useFactory: (h5pEditor: H5PEditor) => { + const h5pAjaxEndpoint = new H5PAjaxEndpoint(h5pEditor); + + return h5pAjaxEndpoint; + }, +}; diff --git a/apps/server/src/modules/h5p-editor/provider/h5p-editor.provider.ts b/apps/server/src/modules/h5p-editor/provider/h5p-editor.provider.ts new file mode 100644 index 00000000000..d7b3e4e5668 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/provider/h5p-editor.provider.ts @@ -0,0 +1,37 @@ +import { H5PEditor, cacheImplementations } from '@lumieducation/h5p-server'; + +import { IH5PEditorOptions, ITranslationFunction } from '@lumieducation/h5p-server/build/src/types'; +import { h5pConfig, h5pUrlGenerator } from '../service/config/h5p-service-config'; +import { ContentStorage, Translator, LibraryStorage, TemporaryFileStorage } from '../service'; + +export const H5PEditorProvider = { + provide: H5PEditor, + inject: [ContentStorage, LibraryStorage, TemporaryFileStorage], + async useFactory( + contentStorage: ContentStorage, + libraryStorage: LibraryStorage, + temporaryStorage: TemporaryFileStorage + ) { + const cache = new cacheImplementations.CachedKeyValueStorage('kvcache'); + + const h5pOptions: IH5PEditorOptions = { + enableHubLocalization: true, + enableLibraryNameLocalization: true, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const translationFunction: ITranslationFunction = await Translator.translate(); + const h5pEditor = new H5PEditor( + cache, + h5pConfig, + libraryStorage, + contentStorage, + temporaryStorage, + translationFunction, + h5pUrlGenerator, + h5pOptions + ); + h5pEditor.setRenderer((model) => model); + + return h5pEditor; + }, +}; diff --git a/apps/server/src/modules/h5p-editor/provider/h5p-player.provider.ts b/apps/server/src/modules/h5p-editor/provider/h5p-player.provider.ts new file mode 100644 index 00000000000..1f3c83db6f3 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/provider/h5p-player.provider.ts @@ -0,0 +1,27 @@ +import { H5PPlayer, ITranslationFunction } from '@lumieducation/h5p-server'; + +import { h5pConfig, h5pUrlGenerator } from '../service/config/h5p-service-config'; +import { ContentStorage } from '../service/contentStorage.service'; +import { Translator } from '../service/h5p-translator.service'; +import { LibraryStorage } from '../service/libraryStorage.service'; + +export const H5PPlayerProvider = { + provide: H5PPlayer, + inject: [ContentStorage, LibraryStorage], + useFactory: async (contentStorage: ContentStorage, libraryStorage: LibraryStorage) => { + const translationFunction: ITranslationFunction = await Translator.translate(); + const h5pPlayer = new H5PPlayer( + libraryStorage, + contentStorage, + h5pConfig, + undefined, + h5pUrlGenerator, + translationFunction, + undefined + ); + + h5pPlayer.setRenderer((model) => model); + + return h5pPlayer; + }, +}; diff --git a/apps/server/src/modules/h5p-editor/provider/index.ts b/apps/server/src/modules/h5p-editor/provider/index.ts new file mode 100644 index 00000000000..db078ea6d15 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/provider/index.ts @@ -0,0 +1,3 @@ +export * from './h5p-editor.provider'; +export * from './h5p-player.provider'; +export * from './h5p-ajax-endpoint.provider'; diff --git a/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts b/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts new file mode 100644 index 00000000000..e06836ced99 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts @@ -0,0 +1,104 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections, h5pContentFactory } from '@shared/testing'; +import { H5PContent } from '../entity'; +import { H5PContentRepo } from './h5p-content.repo'; + +const contentSortFunction = ({ id: aId }: H5PContent, { id: bId }: H5PContent) => aId.localeCompare(bId); + +describe('ContentRepo', () => { + let module: TestingModule; + let repo: H5PContentRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot({ entities: [H5PContent] })], + providers: [H5PContentRepo], + }).compile(); + + repo = module.get(H5PContentRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(H5PContent); + }); + + describe('createContentMetadata', () => { + it('should be able to retrieve entity', async () => { + const h5pContent = h5pContentFactory.build(); + await em.persistAndFlush(h5pContent); + + const result = await repo.findById(h5pContent.id); + + expect(result).toBeDefined(); + expect(result).toEqual(h5pContent); + }); + }); + + describe('findById', () => { + it('should be able to retrieve entity', async () => { + const h5pContent = h5pContentFactory.build(); + await em.persistAndFlush(h5pContent); + + const result = await repo.findById(h5pContent.id); + + expect(result).toBeDefined(); + expect(result).toEqual(h5pContent); + }); + + it('should fail if entity does not exist', async () => { + const id = 'wrong-id'; + + const findById = repo.findById(id); + + await expect(findById).rejects.toThrow(); + }); + }); + + describe('existsOne', () => { + it('should return true if entity exists', async () => { + const h5pContent = h5pContentFactory.build(); + await em.persistAndFlush(h5pContent); + + const result = await repo.existsOne(h5pContent.id); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + }); + }); + + describe('deleteContent', () => { + it('should delete data', async () => { + const h5pContent = h5pContentFactory.build(); + await em.persistAndFlush(h5pContent); + + await repo.deleteContent(h5pContent); + + const findById = repo.findById(h5pContent.id); + await expect(findById).rejects.toThrow(); + }); + }); + + describe('getAllContents', () => { + it('should return all metadata', async () => { + const h5pContent = h5pContentFactory.buildList(10); + await em.persistAndFlush(h5pContent); + + const results = await repo.getAllContents(); + + expect(results).toHaveLength(10); + expect(results.sort(contentSortFunction)).toStrictEqual(h5pContent.sort(contentSortFunction)); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.ts b/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.ts new file mode 100644 index 00000000000..6713aad5d3a --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { BaseRepo } from '@shared/repo/base.repo'; +import { H5PContent } from '../entity'; + +@Injectable() +export class H5PContentRepo extends BaseRepo { + get entityName() { + return H5PContent; + } + + async existsOne(contentId: EntityId): Promise { + const entityCount = await this._em.count(this.entityName, { id: contentId }); + + return entityCount === 1; + } + + async deleteContent(content: H5PContent): Promise { + return this.delete(content); + } + + async findById(contentId: EntityId): Promise { + return this._em.findOneOrFail(this.entityName, { id: contentId }); + } + + async getAllContents(): Promise { + return this._em.find(this.entityName, {}); + } +} diff --git a/apps/server/src/modules/h5p-editor/repo/index.ts b/apps/server/src/modules/h5p-editor/repo/index.ts new file mode 100644 index 00000000000..7d38e6ba404 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/index.ts @@ -0,0 +1,3 @@ +export * from './h5p-content.repo'; +export * from './library.repo'; +export * from './temporary-file.repo'; diff --git a/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts b/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts new file mode 100644 index 00000000000..78d219aa557 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts @@ -0,0 +1,178 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections } from '@shared/testing'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; + +import { ILibraryMetadata } from '@lumieducation/h5p-server'; +import { LibraryRepo } from './library.repo'; +import { FileMetadata, InstalledLibrary } from '../entity'; + +describe('LibraryRepo', () => { + let module: TestingModule; + let libraryRepo: LibraryRepo; + let addonLibVersionOne: InstalledLibrary; + let addonLibVersionOneDuplicate: InstalledLibrary; + let addonLibVersionTwo: InstalledLibrary; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot({ entities: [InstalledLibrary] })], + providers: [LibraryRepo], + }).compile(); + libraryRepo = module.get(LibraryRepo); + em = module.get(EntityManager); + + const testingLibMetadataVersionOne: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'testing', + majorVersion: 1, + minorVersion: 2, + }; + const testingLibVersionOne = new InstalledLibrary(testingLibMetadataVersionOne); + testingLibVersionOne.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadataVersionOne: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'addonVersionOne', + majorVersion: 1, + minorVersion: 2, + }; + const addonLibMetadataVersionOneDuplicate: ILibraryMetadata = { + runnable: false, + title: 'Duplicate', + patchVersion: 3, + machineName: 'addonVersionOne', + majorVersion: 1, + minorVersion: 2, + }; + addonLibVersionOne = new InstalledLibrary(addonLibMetadataVersionOne); + addonLibVersionOne.addTo = { player: { machineNames: [testingLibVersionOne.machineName] } }; + + addonLibVersionOneDuplicate = new InstalledLibrary(addonLibMetadataVersionOneDuplicate); + addonLibVersionOneDuplicate.addTo = { player: { machineNames: [testingLibVersionOne.machineName] } }; + + const testingLibMetadataVersionTwo: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 4, + machineName: 'addonVersionTwo', + majorVersion: 2, + minorVersion: 3, + }; + const testingLibVersionTwo = new InstalledLibrary(testingLibMetadataVersionTwo); + testingLibVersionTwo.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadataVersionTwo: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 4, + machineName: 'addonVersionTwo', + majorVersion: 2, + minorVersion: 3, + }; + addonLibVersionTwo = new InstalledLibrary(addonLibMetadataVersionTwo); + addonLibVersionTwo.addTo = { player: { machineNames: [testingLibVersionTwo.machineName] } }; + + await libraryRepo.createLibrary(addonLibVersionOne); + await libraryRepo.createLibrary(addonLibVersionTwo); + }); + + afterAll(async () => { + await cleanupCollections(em); + await module.close(); + }); + + describe('createLibrary', () => { + it('should save a Library', async () => { + const saveSpy = jest.spyOn(libraryRepo, 'save').mockResolvedValueOnce(undefined); + await libraryRepo.createLibrary(addonLibVersionOne); + expect(saveSpy).toHaveBeenCalledWith(addonLibVersionOne); + saveSpy.mockRestore(); + }); + }); + + describe('getAll', () => { + it('should get all libaries', async () => { + const result = await libraryRepo.getAll(); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + }); + }); + + describe('findByName', () => { + it('should get libaries by name', async () => { + const result = await libraryRepo.findByName('addonVersionTwo'); + expect(result).toBeDefined(); + expect(result).toEqual([addonLibVersionTwo]); + }); + }); + + describe('findOneByNameAndVersionOrFail', () => { + it('should get library', async () => { + const result = await libraryRepo.findOneByNameAndVersionOrFail('addonVersionOne', 1, 2); + expect(result).toBeDefined(); + }); + + it('should throw error', async () => { + try { + await libraryRepo.findOneByNameAndVersionOrFail('notexistinglibrary', 1, 2); + fail('Expected Error'); + } catch (error) { + expect(error).toBeDefined(); + } + }); + it('should throw error', async () => { + try { + await libraryRepo.createLibrary(addonLibVersionOneDuplicate); + await libraryRepo.findOneByNameAndVersionOrFail('addonVersionOne', 1, 2); + fail('Expected Error'); + } catch (error) { + expect(error).toBeDefined(); + expect(error).toEqual(new Error('Multiple libraries with the same name and version found')); + } + }); + }); + + describe('findNewestByNameAndVersion', () => { + it('should get a library by name and version', async () => { + const result = await libraryRepo.findNewestByNameAndVersion('addonVersionTwo', 2, 3); + expect(result).toBeDefined(); + expect(result).toEqual(addonLibVersionTwo); + }); + }); + + describe('findByNameAndExactVersion', () => { + it('should get a library by name and exact version', async () => { + const result = await libraryRepo.findByNameAndExactVersion('addonVersionTwo', 2, 3, 4); + expect(result).toBeDefined(); + expect(result).toEqual(addonLibVersionTwo); + }); + it('should throw error', async () => { + try { + await libraryRepo.findByNameAndExactVersion('addonVersionOne', 1, 2, 3); + fail('Expected Error'); + } catch (error) { + expect(error).toBeDefined(); + expect(error).toEqual(new Error('too many libraries with same name and version')); + } + }); + it('should return null', async () => { + const result = await libraryRepo.findByNameAndExactVersion('addonVersionTwo', 99, 3, 4); + expect(result).toBeDefined(); + expect(result).toEqual(null); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/repo/library.repo.ts b/apps/server/src/modules/h5p-editor/repo/library.repo.ts new file mode 100644 index 00000000000..01aa6eddc4d --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/library.repo.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { BaseRepo } from '@shared/repo/base.repo'; +import { InstalledLibrary } from '../entity'; + +@Injectable() +export class LibraryRepo extends BaseRepo { + get entityName() { + return InstalledLibrary; + } + + async createLibrary(library: InstalledLibrary): Promise { + const entity = this.create(library); + await this.save(entity); + } + + async getAll(): Promise { + return this._em.find(this.entityName, {}); + } + + async findOneByNameAndVersionOrFail( + machineName: string, + majorVersion: number, + minorVersion: number + ): Promise { + const libs = await this._em.find(this.entityName, { machineName, majorVersion, minorVersion }); + if (libs.length === 1) { + return libs[0]; + } + if (libs.length === 0) { + throw new Error('Library not found'); + } + throw new Error('Multiple libraries with the same name and version found'); + } + + async findByName(machineName: string): Promise { + return this._em.find(this.entityName, { machineName }); + } + + async findNewestByNameAndVersion( + machineName: string, + majorVersion: number, + minorVersion: number + ): Promise { + const libs = await this._em.find(this.entityName, { + machineName, + majorVersion, + minorVersion, + }); + let latest: InstalledLibrary | null = null; + for (const lib of libs) { + if (latest === null || lib.patchVersion > latest.patchVersion) { + latest = lib; + } + } + return latest; + } + + async findByNameAndExactVersion( + machineName: string, + majorVersion: number, + minorVersion: number, + patchVersion: number + ): Promise { + const [libs, count] = await this._em.findAndCount(this.entityName, { + machineName, + majorVersion, + minorVersion, + patchVersion, + }); + if (count > 1) { + throw new Error('too many libraries with same name and version'); + } + if (count === 1) { + return libs[0]; + } + return null; + } +} diff --git a/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts b/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts new file mode 100644 index 00000000000..ccaff625d66 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts @@ -0,0 +1,127 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections, h5pTemporaryFileFactory } from '@shared/testing'; +import { H5pEditorTempFile } from '../entity'; +import { TemporaryFileRepo } from './temporary-file.repo'; + +describe('TemporaryFileRepo', () => { + let module: TestingModule; + let repo: TemporaryFileRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot({ entities: [H5pEditorTempFile] })], + providers: [TemporaryFileRepo], + }).compile(); + + repo = module.get(TemporaryFileRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(H5pEditorTempFile); + }); + + describe('createTemporaryFile', () => { + it('should be able to retrieve entity', async () => { + const tempFile = h5pTemporaryFileFactory.build(); + await em.persistAndFlush(tempFile); + + const result = await repo.findById(tempFile.id); + + expect(result).toBeDefined(); + expect(result).toEqual(tempFile); + }); + }); + + describe('findByUserAndFilename', () => { + it('should be able to retrieve entity', async () => { + const tempFile = h5pTemporaryFileFactory.build(); + await em.persistAndFlush(tempFile); + + const result = await repo.findByUserAndFilename(tempFile.ownedByUserId, tempFile.filename); + + expect(result).toBeDefined(); + expect(result).toEqual(tempFile); + }); + + it('should fail if entity does not exist', async () => { + const user = 'wrong-user-id'; + const filename = 'file.txt'; + + const findBy = repo.findByUserAndFilename(user, filename); + + await expect(findBy).rejects.toThrow(); + }); + }); + + describe('findAllByUserAndFilename', () => { + it('should be able to retrieve entity', async () => { + const tempFile = h5pTemporaryFileFactory.build(); + await em.persistAndFlush(tempFile); + + const result = await repo.findAllByUserAndFilename(tempFile.ownedByUserId, tempFile.filename); + + expect(result).toBeDefined(); + expect(result).toEqual([tempFile]); + }); + + it('should return empty array', async () => { + const user = 'wrong-user-id'; + const filename = 'file.txt'; + + const findBy = await repo.findAllByUserAndFilename(user, filename); + + expect(findBy).toEqual([]); + }); + }); + + describe('findExpired', () => { + it('should return expired files', async () => { + const [expiredFile, validFile] = [h5pTemporaryFileFactory.isExpired().build(), h5pTemporaryFileFactory.build()]; + await em.persistAndFlush([expiredFile, validFile]); + + const result = await repo.findExpired(); + + expect(result.length).toBe(1); + expect(result[0]).toEqual(expiredFile); + }); + }); + + describe('findByUser', () => { + it('should return files for user', async () => { + const [firstFile, secondFile] = [h5pTemporaryFileFactory.build(), h5pTemporaryFileFactory.build()]; + await em.persistAndFlush([firstFile, secondFile]); + + const result = await repo.findByUser(firstFile.ownedByUserId); + + expect(result.length).toBe(1); + expect(result[0]).toEqual(firstFile); + }); + }); + + describe('findExpiredByUser', () => { + it('should return expired files for user', async () => { + const [firstFile, secondFile] = [ + h5pTemporaryFileFactory.isExpired().build(), + h5pTemporaryFileFactory.isExpired().build(), + ]; + await em.persistAndFlush([firstFile, secondFile]); + + const result = await repo.findExpiredByUser(firstFile.ownedByUserId); + + expect(result.length).toBe(1); + expect(result[0]).toEqual(firstFile); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.ts b/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.ts new file mode 100644 index 00000000000..ae6966345d9 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { BaseRepo } from '@shared/repo/base.repo'; +import { H5pEditorTempFile } from '../entity'; + +@Injectable() +export class TemporaryFileRepo extends BaseRepo { + get entityName() { + return H5pEditorTempFile; + } + + async findByUserAndFilename(userId: EntityId, filename: string): Promise { + return this._em.findOneOrFail(this.entityName, { ownedByUserId: userId, filename }); + } + + async findAllByUserAndFilename(userId: EntityId, filename: string): Promise { + return this._em.find(this.entityName, { ownedByUserId: userId, filename }); + } + + async findExpired(): Promise { + const now = new Date(); + return this._em.find(this.entityName, { expiresAt: { $lt: now } }); + } + + async findByUser(userId: EntityId): Promise { + return this._em.find(this.entityName, { ownedByUserId: userId }); + } + + async findExpiredByUser(userId: EntityId): Promise { + const now = new Date(); + return this._em.find(this.entityName, { $and: [{ ownedByUserId: userId }, { expiresAt: { $lt: now } }] }); + } +} diff --git a/apps/server/src/modules/h5p-editor/service/config/h5p-service-config.ts b/apps/server/src/modules/h5p-editor/service/config/h5p-service-config.ts new file mode 100644 index 00000000000..f9c8063dffd --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/config/h5p-service-config.ts @@ -0,0 +1,27 @@ +import { H5PConfig, UrlGenerator } from '@lumieducation/h5p-server'; + +const API_BASE = '/api/v3/h5p-editor'; +const STATIC_FILES_BASE = '/h5pstatics'; + +export const h5pConfig = new H5PConfig(undefined, { + baseUrl: '', + + ajaxUrl: `${API_BASE}/ajax`, + contentFilesUrl: `${API_BASE}/content`, + contentFilesUrlPlayerOverride: undefined, + contentUserDataUrl: `${API_BASE}/contentUserData`, + downloadUrl: undefined, + librariesUrl: `${API_BASE}/libraries`, + paramsUrl: `${API_BASE}/params`, + playUrl: `${API_BASE}/play`, + setFinishedUrl: `${API_BASE}/finishedData`, + temporaryFilesUrl: `${API_BASE}/temp-files`, + + coreUrl: `${STATIC_FILES_BASE}/core`, + editorLibraryUrl: `${STATIC_FILES_BASE}/editor`, + + contentUserStateSaveInterval: false, + setFinishedEnabled: false, +}); + +export const h5pUrlGenerator = new UrlGenerator(h5pConfig); diff --git a/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts new file mode 100644 index 00000000000..a02c8867cd0 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts @@ -0,0 +1,928 @@ +import { HeadObjectCommandOutput } from '@aws-sdk/client-s3'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { IContentMetadata, ILibraryName, IUser, LibraryName } from '@lumieducation/h5p-server'; +import { HttpException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IEntity } from '@shared/domain'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { ObjectID } from 'bson'; +import { Readable } from 'stream'; +import { GetH5PFileResponse } from '../controller/dto'; +import { H5PContent, H5PContentParentType, IH5PContentProperties } from '../entity'; +import { H5P_CONTENT_S3_CONNECTION } from '../h5p-editor.config'; +import { H5PContentRepo } from '../repo'; +import { H5PContentParentParams, LumiUserWithContentData } from '../types/lumi-types'; +import { ContentStorage } from './contentStorage.service'; + +const helpers = { + buildMetadata( + title: string, + mainLibrary: string, + preloadedDependencies: ILibraryName[] = [], + dynamicDependencies?: ILibraryName[], + editorDependencies?: ILibraryName[] + ): IContentMetadata { + return { + defaultLanguage: 'de-DE', + license: 'Unlicensed', + title, + dynamicDependencies, + editorDependencies, + embedTypes: ['iframe'], + language: 'de-DE', + mainLibrary, + preloadedDependencies, + }; + }, + + buildContent(n = 0) { + const metadata = helpers.buildMetadata(`Content #${n}`, `Library-${n}.0`); + const content = { + data: `Data #${n}`, + }; + const h5pContentProperties: IH5PContentProperties = { + creatorId: new ObjectID().toString(), + parentId: new ObjectID().toString(), + schoolId: new ObjectID().toString(), + metadata, + content, + parentType: H5PContentParentType.Lesson, + }; + const h5pContent = new H5PContent(h5pContentProperties); + + return { + withID(id?: number) { + const objectId = new ObjectID(id); + h5pContent._id = objectId; + h5pContent.id = objectId.toString(); + + return h5pContent; + }, + new() { + return h5pContent; + }, + }; + }, + + createUser() { + return { + canCreateRestricted: false, + canInstallRecommended: false, + canUpdateAndInstallLibraries: false, + email: 'example@schul-cloud.org', + id: '12345', + name: 'Example User', + type: 'user', + }; + }, + + repoSaveMock: async (entities: Entity | Entity[]) => { + if (!Array.isArray(entities)) { + entities = [entities]; + } + + for (const entity of entities) { + if (!entity._id) { + const id = new ObjectID(); + entity._id = id; + entity.id = id.toString(); + } + } + + return Promise.resolve(); + }, +}; + +describe('ContentStorage', () => { + let module: TestingModule; + let service: ContentStorage; + let s3ClientAdapter: DeepMocked; + let contentRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ContentStorage, + { provide: H5PContentRepo, useValue: createMock() }, + { provide: H5P_CONTENT_S3_CONNECTION, useValue: createMock() }, + ], + }).compile(); + + service = module.get(ContentStorage); + s3ClientAdapter = module.get(H5P_CONTENT_S3_CONNECTION); + contentRepo = module.get(H5PContentRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('service should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('addContent', () => { + const setup = () => { + const newContent = helpers.buildContent(0).new(); + const existingContent = helpers.buildContent(0).withID(); + + const iUser: IUser = { + canCreateRestricted: false, + canInstallRecommended: false, + canUpdateAndInstallLibraries: false, + email: 'example@schul-cloud.org', + id: new ObjectID().toHexString(), + name: 'Example User', + type: 'user', + }; + const parentParams: H5PContentParentParams = { + schoolId: new ObjectID().toHexString(), + parentType: H5PContentParentType.Lesson, + parentId: new ObjectID().toHexString(), + }; + const user = new LumiUserWithContentData(iUser, parentParams); + + return { newContent, existingContent, user }; + }; + + describe('WHEN adding new content', () => { + it('should call H5pContentRepo.save', async () => { + const { + newContent: { metadata, content }, + user, + } = setup(); + + await service.addContent(metadata, content, user); + + expect(contentRepo.save).toHaveBeenCalledWith(expect.objectContaining({ metadata, content })); + }); + + it('should return content id', async () => { + const { + newContent: { metadata, content }, + user, + } = setup(); + contentRepo.save.mockImplementationOnce(helpers.repoSaveMock); + + const id = await service.addContent(metadata, content, user); + + expect(id).toBeDefined(); + }); + }); + + describe('WHEN modifying existing content', () => { + it('should call H5pContentRepo.save', async () => { + const { + existingContent, + newContent: { metadata, content }, + user, + } = setup(); + contentRepo.findById.mockResolvedValueOnce(existingContent); + + await service.addContent(metadata, content, user, existingContent.id); + + expect(contentRepo.save).toHaveBeenCalledWith(expect.objectContaining({ metadata, content })); + }); + + it('should save content and return existing content id', async () => { + const { + existingContent, + newContent: { metadata, content }, + user, + } = setup(); + const oldId = existingContent.id; + contentRepo.save.mockImplementationOnce(helpers.repoSaveMock); + contentRepo.findById.mockResolvedValueOnce(existingContent); + + const newId = await service.addContent(metadata, content, user, oldId); + + expect(newId).toEqual(oldId); + expect(existingContent).toEqual(expect.objectContaining({ metadata, content })); + }); + }); + + describe('WHEN saving content fails', () => { + it('should throw an HttpException', async () => { + const { + existingContent: { metadata, content }, + user, + } = setup(); + contentRepo.save.mockRejectedValueOnce(new Error()); + + const addContentPromise = service.addContent(metadata, content, user); + + await expect(addContentPromise).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN finding content fails', () => { + it('should throw an HttpException', async () => { + const { + existingContent: { metadata, content, id }, + user, + } = setup(); + contentRepo.findById.mockRejectedValueOnce(new Error()); + + const addContentPromise = service.addContent(metadata, content, user, id); + + await expect(addContentPromise).rejects.toThrow(HttpException); + }); + }); + }); + + describe('addFile', () => { + const setup = () => { + const filename = 'filename.txt'; + const stream = Readable.from('content'); + + const contentID = new ObjectID(); + const contentIDString = contentID.toString(); + + const user = helpers.createUser(); + + const fileCreateError = new Error('Could not create file'); + + return { + filename, + stream, + contentID, + contentIDString, + user, + fileCreateError, + }; + }; + + describe('WHEN adding a file to existing content', () => { + it('should check if the content exists', async () => { + const { contentIDString, filename, stream } = setup(); + + await service.addFile(contentIDString, filename, stream); + + expect(contentRepo.existsOne).toBeCalledWith(contentIDString); + }); + + it('should call S3ClientAdapter.create', async () => { + const { contentIDString, filename, stream } = setup(); + contentRepo.existsOne.mockResolvedValueOnce(true); + + await service.addFile(contentIDString, filename, stream); + + expect(s3ClientAdapter.create).toBeCalledWith( + expect.stringContaining(filename), + expect.objectContaining({ + name: filename, + data: stream, + mimeType: 'application/json', + }) + ); + }); + }); + + describe('WHEN adding a file to non existant content', () => { + it('should throw NotFoundException', async () => { + const { contentIDString, filename, stream } = setup(); + contentRepo.findById.mockRejectedValueOnce(new Error()); + + const addFilePromise = service.addFile(contentIDString, filename, stream); + + await expect(addFilePromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN S3ClientAdapter throws error', () => { + it('should throw the error', async () => { + const { contentIDString, filename, stream, fileCreateError } = setup(); + contentRepo.existsOne.mockResolvedValueOnce(true); + s3ClientAdapter.create.mockRejectedValueOnce(fileCreateError); + + const addFilePromise = service.addFile(contentIDString, filename, stream); + + await expect(addFilePromise).rejects.toBe(fileCreateError); + }); + }); + + describe('WHEN content id is empty string', () => { + it('should throw error', async () => { + const { filename, stream } = setup(); + + const addFilePromise = service.addFile('', filename, stream); + + await expect(addFilePromise).rejects.toThrow(); + }); + }); + }); + + describe('contentExists', () => { + describe('WHEN content does exist', () => { + it('should return true', async () => { + const content = helpers.buildContent().withID(); + contentRepo.existsOne.mockResolvedValueOnce(true); + + const exists = await service.contentExists(content.id); + + expect(exists).toBe(true); + }); + }); + + describe('WHEN content does not exist', () => { + it('should return false', async () => { + contentRepo.existsOne.mockResolvedValueOnce(false); + + const exists = await service.contentExists(''); + + expect(exists).toBe(false); + }); + }); + }); + + describe('deleteContent', () => { + const setup = () => { + const content = helpers.buildContent().withID(); + + const files = ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt']; + + const user = helpers.createUser(); + // @ts-expect-error test case + s3ClientAdapter.list.mockResolvedValueOnce({ files }); + + return { + content, + files, + user, + }; + }; + + describe('WHEN content exists', () => { + it('should call H5PContentRepo.delete', async () => { + const { content } = setup(); + contentRepo.findById.mockResolvedValueOnce(content); + contentRepo.existsOne.mockResolvedValueOnce(true); + + await service.deleteContent(content.id); + + expect(contentRepo.delete).toHaveBeenCalledWith(content); + }); + + it('should call S3ClientAdapter.deleteFile for every file', async () => { + const { content, files } = setup(); + contentRepo.findById.mockResolvedValueOnce(content); + contentRepo.existsOne.mockResolvedValueOnce(true); + + await service.deleteContent(content.id); + + for (const file of files) { + expect(s3ClientAdapter.delete).toHaveBeenCalledWith([expect.stringContaining(file)]); + } + }); + }); + + describe('WHEN content does not exist', () => { + it('should throw HttpException', async () => { + const { content } = setup(); + contentRepo.findById.mockRejectedValueOnce(new Error()); + + const deletePromise = service.deleteContent(content.id); + + await expect(deletePromise).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN H5PContentRepo.delete throws an error', () => { + it('should throw HttpException', async () => { + const { content } = setup(); + contentRepo.delete.mockRejectedValueOnce(new Error()); + + const deletePromise = service.deleteContent(content.id); + + await expect(deletePromise).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN S3ClientAdapter.delete throws an error', () => { + it('should throw HttpException', async () => { + const { content } = setup(); + s3ClientAdapter.delete.mockRejectedValueOnce(new Error()); + + const deletePromise = service.deleteContent(content.id); + + await expect(deletePromise).rejects.toThrow(HttpException); + }); + }); + }); + + describe('deleteFile', () => { + const setup = () => { + const filename = 'file.txt'; + const invalidFilename = '..test.txt'; + + const user = helpers.createUser(); + + const deleteError = new Error('Could not delete'); + + const contentID = new ObjectID().toString(); + + return { + contentID, + deleteError, + filename, + invalidFilename, + user, + }; + }; + + describe('WHEN deleting a file', () => { + it('should call S3ClientAdapter.delete', async () => { + const { contentID, filename } = setup(); + + await service.deleteFile(contentID, filename); + + expect(s3ClientAdapter.delete).toHaveBeenCalledWith([expect.stringContaining(contentID)]); + }); + }); + + describe('WHEN filename is invalid', () => { + it('should throw error', async () => { + const { contentID, invalidFilename } = setup(); + + const deletePromise = service.deleteFile(contentID, invalidFilename); + + await expect(deletePromise).rejects.toThrow(); + }); + }); + + describe('WHEN S3ClientAdapter throws an error', () => { + it('should throw along the error', async () => { + const { contentID, filename, deleteError } = setup(); + s3ClientAdapter.delete.mockRejectedValueOnce(deleteError); + + const deletePromise = service.deleteFile(contentID, filename); + + await expect(deletePromise).rejects.toBe(deleteError); + }); + }); + }); + + describe('fileExists', () => { + const setup = () => { + const filename = 'file.txt'; + const invalidFilename = '..test.txt'; + + const deleteError = new Error('Could not delete'); + + const contentID = new ObjectID().toString(); + + return { + contentID, + deleteError, + filename, + invalidFilename, + }; + }; + + describe('WHEN file exists', () => { + it('should return true', async () => { + const { contentID, filename } = setup(); + s3ClientAdapter.head.mockResolvedValueOnce(createMock()); + + const exists = await service.fileExists(contentID, filename); + + expect(exists).toBe(true); + }); + }); + + describe('WHEN file does not exist', () => { + it('should return false', async () => { + const { contentID, filename } = setup(); + // s3ClientAdapter.head.mockRejectedValueOnce(new NotFoundException('NoSuchKey')); + s3ClientAdapter.get.mockRejectedValue(new NotFoundException('NoSuchKey')); + + const exists = await service.fileExists(contentID, filename); + + expect(exists).toBe(false); + }); + }); + + describe('WHEN S3ClientAdapter.head throws error', () => { + it('should throw HttpException', async () => { + const { contentID, filename } = setup(); + s3ClientAdapter.get.mockRejectedValueOnce(new Error()); + + const existsPromise = service.fileExists(contentID, filename); + + await expect(existsPromise).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN filename is invalid', () => { + it('should throw error', async () => { + const { contentID, invalidFilename } = setup(); + + const existsPromise = service.fileExists(contentID, invalidFilename); + + await expect(existsPromise).rejects.toThrow(); + }); + }); + }); + + describe('getFileStats', () => { + const setup = () => { + const filename = 'file.txt'; + + const user = helpers.createUser(); + + const contentID = new ObjectID().toString(); + + const birthtime = new Date(); + const size = 100; + + const headResponse = createMock({ + ContentLength: size, + LastModified: birthtime, + }); + + const headResponseWithoutContentLength = createMock({ + ContentLength: undefined, + LastModified: birthtime, + }); + + const headResponseWithoutLastModified = createMock({ + ContentLength: size, + LastModified: undefined, + }); + + const headError = new Error('Head'); + + return { + size, + birthtime, + contentID, + filename, + user, + headResponse, + headResponseWithoutContentLength, + headResponseWithoutLastModified, + headError, + }; + }; + + describe('WHEN file exists', () => { + it('should return file stats', async () => { + const { filename, contentID, headResponse, size, birthtime } = setup(); + s3ClientAdapter.head.mockResolvedValueOnce(headResponse); + + const stats = await service.getFileStats(contentID, filename); + + expect(stats).toEqual( + expect.objectContaining({ + birthtime, + size, + }) + ); + }); + }); + + describe('WHEN response from S3 is missing ContentLength field', () => { + it('should throw InternalServerError', async () => { + const { filename, contentID, headResponseWithoutContentLength } = setup(); + s3ClientAdapter.head.mockResolvedValueOnce(headResponseWithoutContentLength); + + const statsPromise = service.getFileStats(contentID, filename); + + await expect(statsPromise).rejects.toThrow(InternalServerErrorException); + }); + }); + + describe('WHEN response from S3 is missing LastModified field', () => { + it('should throw InternalServerError', async () => { + const { filename, contentID, headResponseWithoutLastModified } = setup(); + s3ClientAdapter.head.mockResolvedValueOnce(headResponseWithoutLastModified); + + const statsPromise = service.getFileStats(contentID, filename); + + await expect(statsPromise).rejects.toThrow(InternalServerErrorException); + }); + }); + + describe('WHEN S3ClientAdapter.head throws error', () => { + it('should throw the error', async () => { + const { filename, contentID, headError } = setup(); + s3ClientAdapter.head.mockRejectedValueOnce(headError); + + const statsPromise = service.getFileStats(contentID, filename); + + await expect(statsPromise).rejects.toBe(headError); + }); + }); + }); + + describe('getFileStream', () => { + const setup = () => { + const filename = 'testfile.txt'; + const fileStream = Readable.from('content'); + const contentID = new ObjectID().toString(); + const fileResponse = createMock({ data: fileStream }); + const user = helpers.createUser(); + + const getError = new Error('Could not get file'); + + // [start, end, expected range] + const testRanges = [ + [undefined, undefined, '0-'], + [100, undefined, '100-'], + [undefined, 100, '0-100'], + [100, 999, '100-999'], + ] as const; + + return { filename, contentID, fileStream, fileResponse, testRanges, user, getError }; + }; + + describe('WHEN file exists', () => { + it('should S3ClientAdapter.get with range', async () => { + const { testRanges, contentID, filename, user, fileResponse } = setup(); + + for (const range of testRanges) { + s3ClientAdapter.get.mockResolvedValueOnce(fileResponse); + + // eslint-disable-next-line no-await-in-loop + await service.getFileStream(contentID, filename, user, range[0], range[1]); + + expect(s3ClientAdapter.get).toHaveBeenCalledWith(expect.stringContaining(filename), range[2]); + } + }); + + it('should return stream from S3ClientAdapter', async () => { + const { fileStream, contentID, filename, user, fileResponse } = setup(); + s3ClientAdapter.get.mockResolvedValueOnce(fileResponse); + + const stream = await service.getFileStream(contentID, filename, user); + + expect(stream).toBe(fileStream); + }); + }); + + describe('WHEN S3ClientAdapter.get throws error', () => { + it('should throw the error', async () => { + const { contentID, filename, user, getError } = setup(); + s3ClientAdapter.get.mockRejectedValueOnce(getError); + + const streamPromise = service.getFileStream(contentID, filename, user); + + await expect(streamPromise).rejects.toBe(getError); + }); + }); + }); + + describe('getMetadata', () => { + const setup = () => { + const content = helpers.buildContent().withID(); + const { id } = content; + const error = new Error('Content not found'); + + const user = helpers.createUser(); + + return { content, id, user, error }; + }; + + describe('WHEN content exists', () => { + it('should return metadata', async () => { + const { content, id } = setup(); + contentRepo.findById.mockResolvedValueOnce(content); + + const metadata = await service.getMetadata(id); + + expect(metadata).toEqual(content.metadata); + }); + }); + + describe('WHEN content does not exist', () => { + it('should throw error', async () => { + const { id, error } = setup(); + contentRepo.findById.mockRejectedValueOnce(error); + + const metadataPromise = service.getMetadata(id); + + await expect(metadataPromise).rejects.toBe(error); + }); + }); + }); + + describe('getParameters', () => { + const setup = () => { + const content = helpers.buildContent().withID(); + const { id } = content; + const error = new Error('Content not found'); + + const user = helpers.createUser(); + + return { content, id, user, error }; + }; + + describe('WHEN content exists', () => { + it('should return parameters', async () => { + const { content, id } = setup(); + contentRepo.findById.mockResolvedValueOnce(content); + + const parameters = await service.getParameters(id); + + expect(parameters).toEqual(content.content); + }); + }); + + describe('WHEN content does not exist', () => { + it('should throw error', async () => { + const { id, error } = setup(); + contentRepo.findById.mockRejectedValueOnce(error); + + const parametersPromise = service.getParameters(id); + + await expect(parametersPromise).rejects.toBe(error); + }); + }); + }); + + describe('listContent', () => { + const setup = () => { + const getContentsResponse = [1, 2, 3, 4].map((id) => helpers.buildContent().withID(id)); + const contentIds = getContentsResponse.map((content) => content.id); + + const error = new Error('could not list entities'); + + const user = helpers.createUser(); + + return { getContentsResponse, contentIds, user, error }; + }; + + describe('WHEN querying for contents', () => { + it('should return list of IDs', async () => { + const { contentIds, getContentsResponse } = setup(); + contentRepo.getAllContents.mockResolvedValueOnce(getContentsResponse); + + const ids = await service.listContent(); + + expect(ids).toEqual(contentIds); + }); + }); + + describe('WHEN H5PContentRepo.getAllContents throws error', () => { + it('should throw the error', async () => { + const { error } = setup(); + contentRepo.getAllContents.mockRejectedValueOnce(error); + + const listPromise = service.listContent(); + + await expect(listPromise).rejects.toBe(error); + }); + }); + }); + + describe('listFiles', () => { + const setup = () => { + const content = helpers.buildContent().withID(); + const user = helpers.createUser(); + const filenames = ['1.txt', '2.txt']; + const error = new Error('error occured'); + + return { content, filenames, user, error }; + }; + + describe('WHEN content exists', () => { + it('should return list of filenames', async () => { + const { filenames, content } = setup(); + contentRepo.existsOne.mockResolvedValueOnce(true); + // @ts-expect-error test case + s3ClientAdapter.list.mockResolvedValueOnce({ files: filenames }); + + const files = await service.listFiles(content.id); + + expect(files).toEqual(filenames); + }); + }); + + describe('WHEN content does not exist', () => { + it('should throw HttpException', async () => { + const { content } = setup(); + contentRepo.existsOne.mockResolvedValueOnce(false); + + const listPromise = service.listFiles(content.id); + + await expect(listPromise).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN S3ClientAdapter.list throws error', () => { + it('should throw the error', async () => { + const { content, error } = setup(); + contentRepo.existsOne.mockResolvedValueOnce(true); + s3ClientAdapter.list.mockRejectedValueOnce(error); + + const listPromise = service.listFiles(content.id); + + await expect(listPromise).rejects.toBe(error); + }); + }); + + describe('WHEN ID is empty string', () => { + it('should throw error', async () => { + const listPromise = service.listFiles(''); + + await expect(listPromise).rejects.toThrow(); + }); + }); + }); + + describe('getUsage', () => { + const setup = () => { + const library = 'TEST.Library-1.0'; + const libraryName = LibraryName.fromUberName(library); + + const contentMain = helpers.buildContent(0).withID(0); + const content1 = helpers.buildContent(1).withID(1); + const content2 = helpers.buildContent(2).withID(2); + const content3 = helpers.buildContent(3).withID(3); + const content4 = helpers.buildContent(4).withID(4); + + contentMain.metadata.mainLibrary = libraryName.machineName; + contentMain.metadata.preloadedDependencies = [libraryName]; + content1.metadata.preloadedDependencies = [libraryName]; + content2.metadata.editorDependencies = [libraryName]; + content3.metadata.dynamicDependencies = [libraryName]; + + const contents = [contentMain, content1, content2, content3, content4]; + + const findByIdMock = async (id: string) => { + const content = contents.find((c) => c.id === id); + + if (content) { + return Promise.resolve(content); + } + + throw new Error('Not found'); + }; + + const expectedUsage = { asDependency: 3, asMainLibrary: 1 }; + + return { libraryName, findByIdMock, contents, expectedUsage }; + }; + + it('should return the number of times the library is used', async () => { + const { libraryName, contents, findByIdMock, expectedUsage } = setup(); + contentRepo.findById.mockImplementation(findByIdMock); // Will be called multiple times + contentRepo.getAllContents.mockResolvedValueOnce(contents); + + const test = await service.getUsage(libraryName); + + expect(test).toEqual(expectedUsage); + }); + }); + + describe('getUserPermissions (currently unused)', () => { + it('should return array of permissions', async () => { + // const user = helpers.createUser(); + + // This method is currently unused and will be changed later + const permissions = await service.getUserPermissions(); + + expect(permissions.length).toBeGreaterThan(0); + }); + }); + + describe('private methods', () => { + describe('WHEN calling getContentPath with invalid parameters', () => { + it('should throw error', async () => { + // Test private getContentPath using listFiles + contentRepo.existsOne.mockResolvedValueOnce(true); + const promise = service.listFiles(''); + await expect(promise).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN calling getFilePath with invalid parameters', () => { + it('should throw error', async () => { + // Test private getFilePath using fileExists + const missingContentID = service.fileExists('', 'filename'); + await expect(missingContentID).rejects.toThrow(HttpException); + + const missingFilename = service.fileExists('id', ''); + await expect(missingFilename).rejects.toThrow(HttpException); + }); + }); + + describe('WHEN calling checkFilename with invalid parameters', () => { + it('should throw error', async () => { + // Test private checkFilename using deleteFile + const invalidChars = service.deleteFile('id', 'ex#ample.txt'); + await expect(invalidChars).rejects.toThrow(HttpException); + + const includesDoubleDot = service.deleteFile('id', '../test.txt'); + await expect(includesDoubleDot).rejects.toThrow(HttpException); + + const startsWithSlash = service.deleteFile('id', '/example.txt'); + await expect(startsWithSlash).rejects.toThrow(HttpException); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts b/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts new file mode 100644 index 00000000000..95895b867c8 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts @@ -0,0 +1,305 @@ +import { + ContentId, + IContentMetadata, + IContentStorage, + IFileStats, + ILibraryName, + IUser as ILumiUser, + LibraryName, + Permission, +} from '@lumieducation/h5p-server'; +import { + HttpException, + Inject, + Injectable, + InternalServerErrorException, + NotAcceptableException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { ErrorUtils } from '@src/core/error/utils'; +import { Readable } from 'stream'; +import { H5pFileDto } from '../controller/dto/h5p-file.dto'; +import { H5PContent } from '../entity'; +import { H5P_CONTENT_S3_CONNECTION } from '../h5p-editor.config'; +import { H5PContentRepo } from '../repo'; +import { LumiUserWithContentData } from '../types/lumi-types'; + +@Injectable() +export class ContentStorage implements IContentStorage { + constructor( + private readonly repo: H5PContentRepo, + @Inject(H5P_CONTENT_S3_CONNECTION) private readonly storageClient: S3ClientAdapter + ) {} + + private async createOrUpdateContent( + contentId: ContentId, + user: LumiUserWithContentData, + metadata: IContentMetadata, + content: unknown + ): Promise { + let h5pContent: H5PContent; + + if (contentId) { + h5pContent = await this.repo.findById(contentId); + h5pContent.metadata = metadata; + h5pContent.content = content; + } else { + h5pContent = new H5PContent({ + parentType: user.contentParentType, + parentId: user.contentParentId, + creatorId: user.id, + schoolId: user.schoolId, + metadata, + content, + }); + } + return h5pContent; + } + + public async addContent( + metadata: IContentMetadata, + content: unknown, + user: LumiUserWithContentData, + contentId?: ContentId | undefined + ): Promise { + try { + const h5pContent = await this.createOrUpdateContent(contentId as string, user, metadata, content); + await this.repo.save(h5pContent); + + return h5pContent.id; + } catch (error) { + throw new HttpException('message', 500, { + cause: new InternalServerErrorException(error as string, 'ContentStorage:addContent'), + }); + } + } + + public async addFile(contentId: string, filename: string, stream: Readable): Promise { + this.checkFilename(filename); + + const contentExists = await this.contentExists(contentId); + if (!contentExists) { + throw new NotFoundException('The content does not exist'); + } + + const fullPath = this.getFilePath(contentId, filename); + const file: H5pFileDto = { + name: filename, + data: stream, + mimeType: 'application/json', + }; + + await this.storageClient.create(fullPath, file); + } + + public async contentExists(contentId: string): Promise { + const exists = await this.repo.existsOne(contentId); + + return exists; + } + + public async deleteContent(contentId: string): Promise { + try { + const h5pContent = await this.repo.findById(contentId); + + const fileList = await this.listFiles(contentId); + const fileDeletePromises = fileList.map((file) => this.deleteFile(contentId, file)); + + await Promise.all([this.repo.delete(h5pContent), ...fileDeletePromises]); + } catch (error) { + throw new HttpException('message', 500, { + cause: new InternalServerErrorException(error as string, 'ContentStorage:addContent'), + }); + } + } + + public async deleteFile(contentId: string, filename: string): Promise { + this.checkFilename(filename); + const filePath = this.getFilePath(contentId, filename); + await this.storageClient.delete([filePath]); + } + + public async fileExists(contentId: string, filename: string): Promise { + this.checkFilename(filename); + + const filePath = this.getFilePath(contentId, filename); + + return this.exists(filePath); + } + + public async getFileStats(contentId: string, file: string): Promise { + const filePath = this.getFilePath(contentId, file); + const { ContentLength, LastModified } = await this.storageClient.head(filePath); + + if (ContentLength === undefined || LastModified === undefined) { + throw new InternalServerErrorException( + { ContentLength, LastModified }, + 'ContentStorage:getFileStats ContentLength or LastModified are undefined' + ); + } + + const fileStats: IFileStats = { + birthtime: LastModified, + size: ContentLength, + }; + + return fileStats; + } + + public async getFileStream( + contentId: string, + file: string, + _user: ILumiUser, + rangeStart = 0, + rangeEnd?: number + ): Promise { + const filePath = this.getFilePath(contentId, file); + + let range: string; + if (rangeEnd === undefined) { + // Open ended range + range = `${rangeStart}-`; + } else { + // Closed range + range = `${rangeStart}-${rangeEnd}`; + } + + const fileResponse = await this.storageClient.get(filePath, range); + return fileResponse.data; + } + + public async getMetadata(contentId: string): Promise { + const h5pContent = await this.repo.findById(contentId); + return h5pContent.metadata; + } + + public async getParameters(contentId: string): Promise { + const h5pContent = await this.repo.findById(contentId); + return h5pContent.content; + } + + public async getUsage(library: ILibraryName): Promise<{ asDependency: number; asMainLibrary: number }> { + const contentIds = await this.listContent(); + const result = await this.resolveDependecies(contentIds, library); + return result; + } + + public getUserPermissions(): Promise { + const permissions = [Permission.Delete, Permission.Download, Permission.Edit, Permission.Embed, Permission.View]; + + return Promise.resolve(permissions); + } + + public async listContent(): Promise { + const contentList = await this.repo.getAllContents(); + + const contentIDs = contentList.map((c) => c.id); + return contentIDs; + } + + public async listFiles(contentId: string): Promise { + const contentExists = await this.contentExists(contentId); + if (!contentExists) { + throw new HttpException('message', 404, { + cause: new NotFoundException('Content could not be found'), + }); + } + + const path = this.getContentPath(contentId); + const { files } = await this.storageClient.list({ path }); + + return files; + } + + private async exists(checkPath: string): Promise { + try { + await this.storageClient.get(checkPath); + } catch (err) { + if (err instanceof NotFoundException) { + return false; + } + + throw new InternalServerErrorException( + null, + ErrorUtils.createHttpExceptionOptions(err, 'ContentStorage:addContent') + ); + } + + return true; + } + + private hasDependencyOn( + metadata: { + dynamicDependencies?: ILibraryName[]; + editorDependencies?: ILibraryName[]; + preloadedDependencies: ILibraryName[]; + }, + library: ILibraryName + ): boolean { + if ( + metadata.preloadedDependencies.some((dep) => LibraryName.equal(dep, library)) || + metadata.editorDependencies?.some((dep) => LibraryName.equal(dep, library)) || + metadata.dynamicDependencies?.some((dep) => LibraryName.equal(dep, library)) + ) { + return true; + } + return false; + } + + private async resolveDependecies( + contentIds: string[], + library: ILibraryName + ): Promise<{ asMainLibrary: number; asDependency: number }> { + let asDependency = 0; + let asMainLibrary = 0; + + const contentMetadataList = await Promise.all(contentIds.map((id) => this.getMetadata(id))); + + for (const contentMetadata of contentMetadataList) { + const isMainLibrary = contentMetadata.mainLibrary === library.machineName; + if (this.hasDependencyOn(contentMetadata, library)) { + if (isMainLibrary) { + asMainLibrary += 1; + } else { + asDependency += 1; + } + } + } + + return { asMainLibrary, asDependency }; + } + + private checkFilename(filename: string): void { + filename = filename.split('.').slice(0, -1).join('.'); + if (/^[a-zA-Z0-9/._-]*$/.test(filename) && !filename.includes('..') && !filename.startsWith('/')) { + return; + } + throw new HttpException('message', 406, { + cause: new NotAcceptableException(`Filename contains forbidden characters ${filename}`), + }); + } + + private getContentPath(contentId: string): string { + if (!contentId) { + throw new HttpException('message', 406, { + cause: new UnprocessableEntityException('COULD_NOT_CREATE_PATH'), + }); + } + + const path = `h5p-content/${contentId}/`; + return path; + } + + private getFilePath(contentId: string, filename: string): string { + if (!contentId || !filename) { + throw new HttpException('message', 406, { + cause: new UnprocessableEntityException('COULD_NOT_CREATE_PATH'), + }); + } + + const path = `${this.getContentPath(contentId)}${filename}`; + return path; + } +} diff --git a/apps/server/src/modules/h5p-editor/service/h5p-translator.service.ts b/apps/server/src/modules/h5p-editor/service/h5p-translator.service.ts new file mode 100644 index 00000000000..0da03a6866f --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/h5p-translator.service.ts @@ -0,0 +1,34 @@ +import { ITranslationFunction } from '@lumieducation/h5p-server'; +import i18next from 'i18next'; +import i18nextFsBackend from 'i18next-fs-backend'; +import path from 'path'; +import { translatorConfig } from '../h5p-editor.config'; + +export const Translator = { + async translate() { + const lumiPackagePath = path.dirname(require.resolve('@lumieducation/h5p-server/package.json')); + const pathBackend = path.join(lumiPackagePath, 'build/assets/translations/{{ns}}/{{lng}}.json'); + + const translationFunction = await i18next.use(i18nextFsBackend).init({ + backend: { + loadPath: pathBackend, + }, + ns: [ + 'client', + 'copyright-semantics', + 'hub', + 'library-metadata', + 'metadata-semantics', + 'mongo-s3-content-storage', + 's3-temporary-storage', + 'server', + 'storage-file-implementations', + ], + preload: translatorConfig.AVAILABLE_LANGUAGES, + }); + + const translate: ITranslationFunction = (key, language) => translationFunction(key, { lng: language }); + + return translate; + }, +}; diff --git a/apps/server/src/modules/h5p-editor/service/index.ts b/apps/server/src/modules/h5p-editor/service/index.ts new file mode 100644 index 00000000000..d3d93b55fee --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/index.ts @@ -0,0 +1,4 @@ +export * from './contentStorage.service'; +export * from './libraryStorage.service'; +export * from './temporary-file-storage.service'; +export * from './h5p-translator.service'; diff --git a/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts new file mode 100644 index 00000000000..afde26d59b0 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts @@ -0,0 +1,765 @@ +import { Readable } from 'stream'; + +import { HeadObjectCommandOutput, ServiceOutputTypes } from '@aws-sdk/client-s3'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5pError, ILibraryMetadata, ILibraryName } from '@lumieducation/h5p-server'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { FileMetadata, InstalledLibrary } from '../entity/library.entity'; +import { H5P_LIBRARIES_S3_CONNECTION } from '../h5p-editor.config'; +import { LibraryRepo } from '../repo/library.repo'; +import { LibraryStorage } from './libraryStorage.service'; + +async function readStream(stream: Readable): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const chunks: any[] = []; + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); + }); +} + +jest.useFakeTimers(); +describe('LibraryStorage', () => { + let module: TestingModule; + let storage: LibraryStorage; + let s3ClientAdapter: DeepMocked; + let repo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LibraryStorage, + { + provide: LibraryRepo, + useValue: createMock(), + }, + { provide: H5P_LIBRARIES_S3_CONNECTION, useValue: createMock() }, + ], + }).compile(); + + storage = module.get(LibraryStorage); + s3ClientAdapter = module.get(H5P_LIBRARIES_S3_CONNECTION); + repo = module.get(LibraryRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + + const installedLibs: InstalledLibrary[] = []; + + repo.getAll.mockImplementation(() => { + const libs: InstalledLibrary[] = []; + for (const lib of installedLibs) { + libs.push(lib); + } + return Promise.resolve(libs); + }); + + repo.findByName.mockImplementation((machineName) => { + const libs: InstalledLibrary[] = []; + for (const lib of installedLibs) { + if (lib.machineName === machineName) { + libs.push(lib); + } + } + return Promise.resolve(libs); + }); + + repo.findByNameAndExactVersion.mockImplementation((machName, major, minor, patch) => { + for (const lib of installedLibs) { + if ( + lib.machineName === machName && + lib.majorVersion === major && + lib.minorVersion === minor && + lib.patchVersion === patch + ) { + return Promise.resolve(lib); + } + } + return Promise.resolve(null); + }); + + repo.findNewestByNameAndVersion.mockImplementation((machName, major, minor) => { + let latest: InstalledLibrary | null = null; + for (const lib of installedLibs) { + if ( + lib.machineName === machName && + lib.majorVersion === major && + lib.minorVersion === minor && + (latest === null || lib.patchVersion > latest.patchVersion) + ) { + latest = lib; + } + } + return Promise.resolve(latest); + }); + + repo.findOneByNameAndVersionOrFail.mockImplementation((machName, major, minor) => { + const libs: InstalledLibrary[] = []; + for (const lib of installedLibs) { + if (lib.machineName === machName && lib.majorVersion === major && lib.minorVersion === minor) { + libs.push(lib); + } + } + if (libs.length === 1) { + return Promise.resolve(libs[0]); + } + if (libs.length === 0) { + throw new Error('Library not found'); + } + throw new Error('Multiple libraries with the same name and version found'); + }); + + repo.createLibrary.mockImplementation((lib) => { + installedLibs.push(lib); + return Promise.resolve(); + }); + + repo.save.mockImplementation((lib) => { + if ('concat' in lib) { + throw Error('Expected InstalledLibrary, not InstalledLibrary[]'); + } + if (installedLibs.indexOf(lib) === -1) { + installedLibs.push(lib); + } + return Promise.resolve(); + }); + + repo.delete.mockImplementation((lib) => { + const index = installedLibs.indexOf(lib as InstalledLibrary); + if (index > -1) { + installedLibs.splice(index, 1); + } else { + throw new Error('Library not found'); + } + return Promise.resolve(); + }); + + const savedFiles: [string, string][] = []; + + s3ClientAdapter.create.mockImplementation(async (filepath, dto) => { + const content = await readStream(dto.data); + savedFiles.push([filepath, content]); + return Promise.resolve({} as ServiceOutputTypes); + }); + + s3ClientAdapter.head.mockImplementation((filepath) => { + for (const file of savedFiles) { + if (file[0] === filepath) { + return Promise.resolve({ contentLength: file[1].length } as unknown as HeadObjectCommandOutput); + } + } + throw new Error(`S3 object under ${filepath} not found`); + }); + + s3ClientAdapter.get.mockImplementation((filepath) => { + for (const file of savedFiles) { + if (file[0] === filepath) { + return Promise.resolve({ + name: file[1], + contentLength: file[1].length, + data: Readable.from(Buffer.from(file[1])), + }); + } + } + throw new Error(`S3 object under ${filepath} not found`); + }); + }); + + const createTestData = () => { + const metadataToName = ({ machineName, majorVersion, minorVersion }: ILibraryMetadata): ILibraryName => { + return { + machineName, + majorVersion, + minorVersion, + }; + }; + const testingLibMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'testing', + majorVersion: 1, + minorVersion: 2, + }; + const testingLib = new InstalledLibrary(testingLibMetadata); + testingLib.files.push( + new FileMetadata('file1', new Date(), 2), + new FileMetadata('file2', new Date(), 4), + new FileMetadata('file3', new Date(), 6) + ); + + const addonLibMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'addon', + majorVersion: 1, + minorVersion: 2, + }; + const addonLib = new InstalledLibrary(addonLibMetadata); + addonLib.addTo = { player: { machineNames: [testingLib.machineName] } }; + + const circularALibMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'circular_a', + majorVersion: 1, + minorVersion: 2, + }; + const circularA = new InstalledLibrary(circularALibMetadata); + const circularBLibMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 3, + machineName: 'circular_b', + majorVersion: 1, + minorVersion: 2, + }; + const circularB = new InstalledLibrary(circularBLibMetadata); + circularA.preloadedDependencies = [metadataToName(circularB)]; + circularB.editorDependencies = [metadataToName(circularA)]; + + const fakeLibraryName: ILibraryName = { machineName: 'fake', majorVersion: 2, minorVersion: 3 }; + + const testingLibDependentAMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 6, + machineName: 'first_dependent', + majorVersion: 2, + minorVersion: 5, + }; + const testingLibDependentA = new InstalledLibrary(testingLibDependentAMetadata); + testingLibDependentA.dynamicDependencies = [metadataToName(testingLib)]; + + const testingLibDependentBMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 6, + machineName: 'second_dependent', + majorVersion: 2, + minorVersion: 5, + }; + const testingLibDependentB = new InstalledLibrary(testingLibDependentBMetadata); + testingLibDependentB.preloadedDependencies = [metadataToName(testingLib)]; + + const libWithNonExistingDependencyMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: 6, + machineName: 'fake_dependency', + majorVersion: 2, + minorVersion: 5, + }; + const libWithNonExistingDependency = new InstalledLibrary(libWithNonExistingDependencyMetadata); + libWithNonExistingDependency.editorDependencies = [fakeLibraryName]; + + return { + libraries: [ + testingLib, + addonLib, + circularA, + circularB, + testingLibDependentA, + testingLibDependentB, + libWithNonExistingDependency, + ], + names: { + testingLib, + addonLib, + fakeLibraryName, + }, + }; + }; + + it('should be defined', () => { + expect(storage).toBeDefined(); + }); + + describe('when managing library metadata', () => { + const setup = async (addLibrary = true) => { + const { + names: { testingLib }, + } = createTestData(); + + if (addLibrary) { + await storage.addLibrary(testingLib, false); + } + + return { testingLib }; + }; + + describe('when adding library', () => { + it('should succeed', async () => { + await setup(); + + expect(repo.createLibrary).toHaveBeenCalled(); + }); + + it('should fail to override existing library', async () => { + const { testingLib } = await setup(); + + repo.findByNameAndExactVersion.mockResolvedValue(testingLib); + + const addLib = storage.addLibrary(testingLib, false); + await expect(addLib).rejects.toThrowError("Can't add library because it already exists"); + }); + }); + + describe('when getting metadata', () => { + it('should succeed if library exists', async () => { + const { testingLib } = await setup(); + + repo.findOneByNameAndVersionOrFail.mockResolvedValue(testingLib); + + const returnedLibrary = await storage.getLibrary(testingLib); + expect(returnedLibrary).toEqual(expect.objectContaining(testingLib)); + }); + + it("should fail if library doesn't exist", async () => { + const { testingLib } = await setup(false); + + repo.findOneByNameAndVersionOrFail.mockImplementation(() => { + throw new Error('Library does not exist'); + }); + + const getLibrary = storage.getLibrary(testingLib); + await expect(getLibrary).rejects.toThrowError(); + }); + }); + + describe('when checking installed status', () => { + it('should return true if library is installed', async () => { + const { testingLib } = await setup(); + + repo.findNewestByNameAndVersion.mockResolvedValue(testingLib); + + const installed = await storage.isInstalled(testingLib); + expect(installed).toBe(true); + }); + + it("should return false if library isn't installed", async () => { + const { testingLib } = await setup(false); + + repo.findNewestByNameAndVersion.mockResolvedValue(null); + + const installed = await storage.isInstalled(testingLib); + expect(installed).toBe(false); + }); + }); + + describe('when updating metadata', () => { + it('should update metadata', async () => { + const { testingLib } = await setup(); + + const libFromDatabaseMetadata: ILibraryMetadata = { + runnable: false, + title: '', + patchVersion: testingLib.patchVersion, + machineName: testingLib.machineName, + majorVersion: testingLib.majorVersion, + minorVersion: testingLib.minorVersion, + }; + const libFromDatabase = new InstalledLibrary(libFromDatabaseMetadata); + + repo.findOneByNameAndVersionOrFail.mockResolvedValue(libFromDatabase); + + testingLib.author = 'Test Author'; + const updatedLibrary = await storage.updateLibrary(testingLib); + const retrievedLibrary = await storage.getLibrary(testingLib); + expect(retrievedLibrary).toEqual(updatedLibrary); + expect(repo.save).toHaveBeenCalled(); + }); + + it("should fail if library doesn't exist", async () => { + const { testingLib } = await setup(false); + + repo.findOneByNameAndVersionOrFail.mockImplementation(() => { + throw new Error('Library is not installed'); + }); + + const updateLibrary = storage.updateLibrary(testingLib); + await expect(updateLibrary).rejects.toThrowError('Library is not installed'); + }); + }); + + describe('when updating additional metadata', () => { + it('should return true if data has changed', async () => { + const { testingLib } = await setup(); + + repo.findOneByNameAndVersionOrFail.mockResolvedValue(testingLib); + + const updated = await storage.updateAdditionalMetadata(testingLib, { restricted: true }); + expect(updated).toBe(true); + }); + + it("should return false if data hasn't changed", async () => { + const { testingLib } = await setup(); + + repo.findOneByNameAndVersionOrFail.mockResolvedValue(testingLib); + + const updated = await storage.updateAdditionalMetadata(testingLib, { restricted: false }); + expect(updated).toBe(false); + }); + + it('should fail if data could not be updated', async () => { + const { testingLib } = await setup(); + + repo.findOneByNameAndVersionOrFail.mockResolvedValue(testingLib); + repo.save.mockImplementation(() => { + throw new Error('Library could not be saved'); + }); + + const updateMetadata = storage.updateAdditionalMetadata(testingLib, { restricted: true }); + await expect(updateMetadata).rejects.toThrowError(); + }); + }); + + describe('when deleting library', () => { + it('should succeed if library exists', async () => { + const { testingLib } = await setup(); + + repo.findOneByNameAndVersionOrFail.mockResolvedValue(testingLib); + repo.delete.mockImplementation(() => { + repo.findOneByNameAndVersionOrFail.mockImplementation(() => { + throw new Error('Library is not installed'); + }); + return Promise.resolve(); + }); + + // @ts-expect-error test case + s3ClientAdapter.list.mockResolvedValueOnce({ files: [] }); + + await storage.deleteLibrary(testingLib); + await expect(storage.getLibrary(testingLib)).rejects.toThrow(); + expect(s3ClientAdapter.delete).toHaveBeenCalled(); + }); + + it("should fail if library doesn't exists", async () => { + const { testingLib } = await setup(false); + + repo.findOneByNameAndVersionOrFail.mockImplementation(() => { + throw new Error('Library is not installed'); + }); + + const deleteLibrary = storage.deleteLibrary(testingLib); + await expect(deleteLibrary).rejects.toThrowError(); + }); + }); + }); + + describe('getLibraryFile', () => { + describe('when getting library.json file', () => { + const setup = async (addLibrary = true) => { + const { + names: { testingLib }, + } = createTestData(); + + if (addLibrary) { + await storage.addLibrary(testingLib, false); + } + const ubername = 'testing-1.2'; + const file = 'library.json'; + + return { testingLib, file, ubername }; + }; + + it('should return library.json file', async () => { + const { testingLib, file, ubername } = await setup(); + repo.findOneByNameAndVersionOrFail.mockResolvedValueOnce(testingLib); + + const result = await storage.getLibraryFile(ubername, file); + + expect(result).toBeDefined(); + expect(result.mimetype).toBeDefined(); + expect(result.mimetype).toEqual('application/json'); + }); + }); + }); + + describe('When getting library dependencies', () => { + const setup = async () => { + const { libraries, names } = createTestData(); + + for await (const library of libraries) { + await storage.addLibrary(library, false); + } + + return names; + }; + + it('should find addon libraries', async () => { + const { addonLib } = await setup(); + + const addons = await storage.listAddons(); + expect(addons).toEqual([addonLib]); + }); + + it('should count dependencies', async () => { + await setup(); + + const dependencies = await storage.getAllDependentsCount(); + expect(dependencies).toEqual({ 'circular_a-1.2': 1, 'testing-1.2': 2, 'fake-2.3': 1 }); + }); + + it('should count dependents for single library', async () => { + const { testingLib } = await setup(); + + const count = await storage.getDependentsCount(testingLib); + expect(count).toBe(2); + }); + + it('should count dependencies for library without dependents', async () => { + const { addonLib } = await setup(); + + const count = await storage.getDependentsCount(addonLib); + expect(count).toBe(0); + }); + }); + + describe('when listing libraries', () => { + const setup = async () => { + const { + libraries, + names: { testingLib }, + } = createTestData(); + + for await (const library of libraries) { + await storage.addLibrary(library, false); + } + + return { libraries, testingLib }; + }; + + it('should return all libraries when no filter is used', async () => { + const { libraries } = await setup(); + + const allLibraries = await storage.getInstalledLibraryNames(); + expect(allLibraries.length).toBe(libraries.length); + }); + + it('should return all libraries with machinename', async () => { + const { testingLib } = await setup(); + + const allLibraries = await storage.getInstalledLibraryNames(testingLib.machineName); + expect(allLibraries.length).toBe(1); + }); + }); + + describe('when managing files', () => { + const setup = async (addLib = true, addFiles = true) => { + const { + names: { testingLib }, + } = createTestData(); + + const testFile = { + name: 'test/abc.json', + content: JSON.stringify({ property: 'value' }), + }; + + if (addLib) { + await storage.addLibrary(testingLib, false); + } + + if (addFiles) { + await storage.addFile(testingLib, testFile.name, Readable.from(Buffer.from(testFile.content))); + } + + return { testingLib, testFile }; + }; + + describe('when adding files', () => { + it('should work', async () => { + await setup(); + }); + + it('should fail on illegal filename', async () => { + const { testingLib } = await setup(); + + const filenames = ['../abc.json', '/test/abc.json']; + + await Promise.all( + filenames.map((filename) => { + const addFile = () => storage.addFile(testingLib, filename, Readable.from(Buffer.from(''))); + return expect(addFile).rejects.toThrow('illegal-filename'); + }) + ); + }); + + describe('when s3 upload error', () => { + it('should throw H5P Error', async () => { + const { testingLib } = await setup(); + const filename = 'test/abc.json'; + + s3ClientAdapter.create.mockImplementationOnce(() => { + throw Error('S3 Exception'); + }); + + const addFile = () => storage.addFile(testingLib, filename, Readable.from(Buffer.from(''))); + return expect(addFile).rejects.toThrow( + new H5pError(`mongo-s3-library-storage:s3-upload-error (ubername: testing-1.2, filename: test/abc.json)`) + ); + }); + }); + }); + + it('should list all files', async () => { + const { testingLib, testFile } = await setup(); + + // @ts-expect-error test + s3ClientAdapter.list.mockResolvedValueOnce({ files: [testFile.name] }); + + const files = await storage.listFiles(testingLib); + expect(files).toContainEqual(expect.stringContaining(testFile.name)); + }); + + describe('when checking if file exists', () => { + it('should return true if it exists', async () => { + const { testingLib, testFile } = await setup(); + + const exists = await storage.fileExists(testingLib, testFile.name); + expect(exists).toBe(true); + }); + + it("should return false if it doesn't exist", async () => { + const { testingLib, testFile } = await setup(true, false); + + const exists = await storage.fileExists(testingLib, testFile.name); + expect(exists).toBe(false); + }); + }); + + describe('when clearing files', () => { + it('should remove all files', async () => { + const { testingLib, testFile } = await setup(); + + // @ts-expect-error test + s3ClientAdapter.list.mockResolvedValueOnce({ files: [testFile.name] }); + + await storage.clearFiles(testingLib); + + expect(s3ClientAdapter.delete).toHaveBeenCalledWith([expect.stringContaining(testFile.name)]); + }); + + it("should fail if library doesn't exist", async () => { + const { testingLib } = await setup(false, false); + + const clearFiles = () => storage.clearFiles(testingLib); + await expect(clearFiles).rejects.toThrow('mongo-s3-library-storage:clear-library-not-found'); + }); + }); + + describe('when retrieving files', () => { + it('should return parsed json', async () => { + const { testingLib, testFile } = await setup(); + + const json = await storage.getFileAsJson(testingLib, testFile.name); + expect(json).toEqual(JSON.parse(testFile.content)); + }); + + it('should return file as string', async () => { + const { testingLib, testFile } = await setup(); + + const fileContent = await storage.getFileAsString(testingLib, testFile.name); + expect(fileContent).toEqual(testFile.content); + }); + + it('should return file as stream', async () => { + const { testingLib, testFile } = await setup(); + + const fileStream = await storage.getFileStream(testingLib, testFile.name); + + const streamContents = await new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const chunks: any[] = []; + fileStream.on('data', (chunk) => chunks.push(chunk)); + fileStream.on('error', reject); + fileStream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); + }); + + expect(streamContents).toEqual(testFile.content); + }); + }); + describe('when getting file stats', () => { + it('should return file stats', async () => { + const { testingLib, testFile } = await setup(); + + const mockStats = { + LastModified: new Date(), + ContentLength: 15, + }; + + // @ts-expect-error partial mock + s3ClientAdapter.head.mockResolvedValueOnce(mockStats); + + const stats = await storage.getFileStats(testingLib, testFile.name); + + expect(stats).toMatchObject({ + size: mockStats.ContentLength, + birthtime: mockStats.LastModified, + }); + }); + + it('should fail if filename is invalid', async () => { + const { testingLib } = await setup(true, false); + + const getStats = storage.getFileStats(testingLib, '../invalid'); + await expect(getStats).rejects.toThrowError('illegal-filename'); + }); + + it('should throw NotFoundException if the file has no content-length or birthtime', async () => { + const { testingLib, testFile } = await setup(); + + s3ClientAdapter.head + // @ts-expect-error partial mock + .mockResolvedValueOnce({ + LastModified: new Date(), + }) + // @ts-expect-error partial mock + .mockResolvedValueOnce({ + ContentLength: 10, + }); + + const undefinedLength = storage.getFileStats(testingLib, testFile.name); + await expect(undefinedLength).rejects.toThrowError(NotFoundException); + + const undefinedBirthtime = storage.getFileStats(testingLib, testFile.name); + await expect(undefinedBirthtime).rejects.toThrow(NotFoundException); + }); + }); + }); + + describe('when getting languages', () => { + const setup = async () => { + const { + names: { testingLib }, + } = createTestData(); + + await storage.addLibrary(testingLib, false); + + const languageFiles = ['en.json', 'de.json']; + const languages = ['en', 'de']; + // @ts-expect-error test + s3ClientAdapter.list.mockResolvedValueOnce({ files: languageFiles }); + + return { testingLib, languages }; + }; + + it('should return a list of languages', async () => { + const { testingLib, languages } = await setup(); + + const supportedLanguages = await storage.getLanguages(testingLib); + expect(supportedLanguages).toEqual(expect.arrayContaining(languages)); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts new file mode 100644 index 00000000000..6839a9d2a27 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts @@ -0,0 +1,452 @@ +import { + H5pError, + LibraryName, + streamToString, + type IAdditionalLibraryMetadata, + type IFileStats, + type IInstalledLibrary, + type ILibraryMetadata, + type ILibraryName, + type ILibraryStorage, +} from '@lumieducation/h5p-server'; +import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import mime from 'mime'; +import path from 'node:path/posix'; +import { Readable } from 'stream'; +import { H5pFileDto } from '../controller/dto'; +import { InstalledLibrary } from '../entity/library.entity'; +import { H5P_LIBRARIES_S3_CONNECTION } from '../h5p-editor.config'; +import { LibraryRepo } from '../repo/library.repo'; + +@Injectable() +export class LibraryStorage implements ILibraryStorage { + /** + * @param + */ + constructor( + private readonly libraryRepo: LibraryRepo, + @Inject(H5P_LIBRARIES_S3_CONNECTION) private readonly s3Client: S3ClientAdapter + ) {} + + /** + * Checks if the filename is absolute or traverses outside the directory. + * Throws an error if the filename is illegal. + * @param filename the requested file + */ + private checkFilename(filename: string): void { + const hasPathTraversal = /\.\.\//.test(filename); + const isAbsolutePath = filename.startsWith('/'); + + if (hasPathTraversal || isAbsolutePath) { + throw new H5pError('illegal-filename', { filename }, 400); + } + } + + private getS3Key(library: ILibraryName, filename: string) { + const uberName = LibraryName.toUberName(library); + const s3Key = `h5p-libraries/${uberName}/${filename}`; + + return s3Key; + } + + /** + * Adds a file to a library. Library metadata must be installed using `installLibrary` first. + * @param library + * @param filename + * @param dataStream + * @returns true if successful + */ + public async addFile(libraryName: ILibraryName, filename: string, dataStream: Readable): Promise { + this.checkFilename(filename); + + const s3Key = this.getS3Key(libraryName, filename); + + try { + await this.s3Client.create( + s3Key, + new H5pFileDto({ + name: s3Key, + mimeType: 'application/octet-stream', + data: dataStream, + }) + ); + } catch (error) { + throw new H5pError( + `mongo-s3-library-storage:s3-upload-error`, + { ubername: LibraryName.toUberName(libraryName), filename }, + 500 + ); + } + + return true; + } + + /** + * Adds the metadata of the library + * @param libraryMetadata + * @param restricted + * @returns The newly created library object + */ + public async addLibrary(libMeta: ILibraryMetadata, restricted: boolean): Promise { + const existingLibrary = await this.libraryRepo.findByNameAndExactVersion( + libMeta.machineName, + libMeta.majorVersion, + libMeta.minorVersion, + libMeta.patchVersion + ); + + if (existingLibrary !== null) { + throw new ConflictException("Can't add library because it already exists"); + } + + const library = new InstalledLibrary(libMeta, restricted, undefined); + + await this.libraryRepo.createLibrary(library); + + return library; + } + + /** + * Removes all files of a library, but keeps the metadata + * @param library + */ + public async clearFiles(libraryName: ILibraryName): Promise { + const isInstalled = await this.isInstalled(libraryName); + + if (!isInstalled) { + throw new H5pError('mongo-s3-library-storage:clear-library-not-found', { + ubername: LibraryName.toUberName(libraryName), + }); + } + + const filesToDelete = await this.listFiles(libraryName, false); + + await this.s3Client.delete(filesToDelete.map((file) => this.getS3Key(libraryName, file))); + } + + /** + * Deletes metadata and all files of the library + * @param library + */ + public async deleteLibrary(libraryName: ILibraryName): Promise { + const isInstalled = await this.isInstalled(libraryName); + + if (!isInstalled) { + throw new H5pError('mongo-s3-library-storage:library-not-found'); + } + + await this.clearFiles(libraryName); + + const library = await this.libraryRepo.findOneByNameAndVersionOrFail( + libraryName.machineName, + libraryName.majorVersion, + libraryName.minorVersion + ); + + await this.libraryRepo.delete(library); + } + + /** + * Checks if the file exists in the library + * @param library + * @param filename + * @returns true if the file exists, false otherwise + */ + public async fileExists(libraryName: ILibraryName, filename: string): Promise { + this.checkFilename(filename); + + try { + await this.s3Client.head(this.getS3Key(libraryName, filename)); + return true; + } catch (error) { + return false; + } + } + + /** + * Counts how often libraries are listed in the dependencies of other libraries and returns a list of the number. + * @returns an object with ubernames as key. + */ + public async getAllDependentsCount(): Promise<{ [ubername: string]: number }> { + const libraries = await this.libraryRepo.getAll(); + const libraryMap = new Map(libraries.map((library) => [LibraryName.toUberName(library), library])); + + // Remove circular dependencies + for (const library of libraries) { + for (const dependency of library.editorDependencies ?? []) { + const ubername = LibraryName.toUberName(dependency); + + const dependencyMetadata = libraryMap.get(ubername); + + if (dependencyMetadata?.preloadedDependencies) { + const index = dependencyMetadata.preloadedDependencies.findIndex((libName) => + LibraryName.equal(libName, library) + ); + + if (index >= 0) { + dependencyMetadata.preloadedDependencies.splice(index, 1); + } + } + } + } + + // Count dependencies + const dependencies: { [ubername: string]: number } = {}; + for (const library of libraries) { + const { preloadedDependencies = [], editorDependencies = [], dynamicDependencies = [] } = library; + + for (const dependency of preloadedDependencies.concat(editorDependencies, dynamicDependencies)) { + const ubername = LibraryName.toUberName(dependency); + dependencies[ubername] = (dependencies[ubername] ?? 0) + 1; + } + } + + return dependencies; + } + + /** + * Counts how many dependents the library has. + * @param library + * @returns the count + */ + public async getDependentsCount(library: ILibraryName): Promise { + const allDependencies = await this.getAllDependentsCount(); + return allDependencies[LibraryName.toUberName(library)] ?? 0; + } + + /** + * Returns the file as a JSON-parsed object + * @param library + * @param file + */ + public async getFileAsJson(library: ILibraryName, file: string): Promise { + const content = await this.getFileAsString(library, file); + return JSON.parse(content) as unknown; + } + + /** + * Returns the file as a utf-8 string + * @param library + * @param file + */ + public async getFileAsString(library: ILibraryName, file: string): Promise { + const stream = await this.getFileStream(library, file); + const data = await streamToString(stream); + return data; + } + + /** + * Returns information about a library file + * @param library + * @param file + */ + public async getFileStats(libraryName: ILibraryName, file: string): Promise { + this.checkFilename(file); + + const s3Key = this.getS3Key(libraryName, file); + const head = await this.s3Client.head(s3Key); + + if (head.LastModified === undefined || head.ContentLength === undefined) { + throw new NotFoundException(); + } + + return { + birthtime: head.LastModified, + size: head.ContentLength, + }; + } + + /** + * Returns a readable stream of the file's contents. + * @param library + * @param file + */ + public async getFileStream(library: ILibraryName, file: string): Promise { + const ubername = LibraryName.toUberName(library); + + const response = await this.getLibraryFile(ubername, file); + + return response.stream; + } + + /** + * Lists all installed libraries or the installed libraries that have the machine name + * @param machineName (optional) only return libraries that have this machine name + */ + public async getInstalledLibraryNames(machineName?: string): Promise { + if (machineName) { + return this.libraryRepo.findByName(machineName); + } + return this.libraryRepo.getAll(); + } + + /** + * Lists all languages supported by a library + * @param library + */ + public async getLanguages(libraryName: ILibraryName): Promise { + const prefix = this.getS3Key(libraryName, 'language'); + + const { files } = await this.s3Client.list({ path: prefix }); + + const jsonFiles = files.filter((file) => path.extname(file) === '.json'); + const languages = jsonFiles.map((file) => path.basename(file, '.json')); + + return languages; + } + + /** + * Returns the library metadata + * @param library + */ + public async getLibrary(library: ILibraryName): Promise { + return this.libraryRepo.findOneByNameAndVersionOrFail( + library.machineName, + library.majorVersion, + library.minorVersion + ); + } + + /** + * Checks if a library is installed + * @param library + */ + public async isInstalled(libraryName: ILibraryName): Promise { + const library = await this.libraryRepo.findNewestByNameAndVersion( + libraryName.machineName, + libraryName.majorVersion, + libraryName.minorVersion + ); + return library !== null; + } + + /** + * Lists all addons that are installed in the system. + */ + public async listAddons(): Promise { + const installedLibraryNames = await this.getInstalledLibraryNames(); + const installedLibraries = await Promise.all(installedLibraryNames.map((addonName) => this.getLibrary(addonName))); + const addons = installedLibraries.filter((library) => library.addTo !== undefined); + + return addons; + } + + /** + * Returns all files that are a part of the library + * @param library + * @param withMetadata wether to include metadata file + * @returns an array of filenames + */ + public async listFiles(libraryName: ILibraryName, withMetadata = true): Promise { + const prefix = this.getS3Key(libraryName, 'language'); + + const { files } = await this.s3Client.list({ path: prefix }); + + if (withMetadata) { + return files.concat('library.json'); + } + + return files; + } + + /** + * Updates the additional metadata properties that are added to the stored libraries. + * @param library + * @param additionalMetadata + */ + public async updateAdditionalMetadata( + libraryName: ILibraryName, + additionalMetadata: Partial + ): Promise { + const library = await this.getLibrary(libraryName); + + let dirty = false; + for (const [property, value] of Object.entries(additionalMetadata)) { + if (value !== library[property]) { + library[property] = value; + dirty = true; + } + } + + // Don't write file if nothing has changed + if (!dirty) { + return false; + } + + await this.libraryRepo.save(library); + + return true; + } + + /** + * Updates the library metadata + * @param libraryMetadata + */ + async updateLibrary(library: ILibraryMetadata): Promise { + const existingLibrary = await this.libraryRepo.findOneByNameAndVersionOrFail( + library.machineName, + library.majorVersion, + library.minorVersion + ); + let dirty = false; + for (const [property, value] of Object.entries(library)) { + if (property !== '_id' && value !== existingLibrary[property]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + existingLibrary[property] = value; + dirty = true; + } + } + if (dirty) { + await this.libraryRepo.save(existingLibrary); + } + + return existingLibrary; + } + + private async getMetadata(library: ILibraryName): Promise { + const result = await this.libraryRepo.findOneByNameAndVersionOrFail( + library.machineName, + library.majorVersion, + library.minorVersion + ); + + return result; + } + + /** + * Returns a file from a library + * @param ubername Library ubername + * @param file file + * @returns a readable stream, mimetype and size + */ + public async getLibraryFile(ubername: string, file: string) { + const libraryName = LibraryName.fromUberName(ubername); + + this.checkFilename(file); + + let result: { stream: Readable | never; mimetype: string; size: number | undefined } | null = null; + + if (file === 'library.json') { + const metadata = await this.getMetadata(libraryName); + const stringifiedMetadata = JSON.stringify(metadata); + const readable = Readable.from(stringifiedMetadata); + + result = { + stream: readable, + mimetype: 'application/json', + size: stringifiedMetadata.length, + }; + } else { + const response = await this.s3Client.get(this.getS3Key(libraryName, file)); + const mimetype = mime.lookup(file, 'application/octet-stream'); + + result = { + stream: response.data, + mimetype, + size: response.contentLength, + }; + } + return result; + } +} diff --git a/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts new file mode 100644 index 00000000000..bcbdfa9945c --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts @@ -0,0 +1,309 @@ +import { ServiceOutputTypes } from '@aws-sdk/client-s3'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { IUser } from '@lumieducation/h5p-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { File, S3ClientAdapter } from '@shared/infra/s3-client'; +import { ReadStream } from 'fs'; +import { Readable } from 'node:stream'; +import { GetH5pFileResponse } from '../controller/dto'; +import { H5pEditorTempFile } from '../entity/h5p-editor-tempfile.entity'; +import { H5P_CONTENT_S3_CONNECTION } from '../h5p-editor.config'; +import { TemporaryFileRepo } from '../repo/temporary-file.repo'; +import { TemporaryFileStorage } from './temporary-file-storage.service'; + +const today = new Date(); +const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1); + +describe('TemporaryFileStorage', () => { + let module: TestingModule; + let storage: TemporaryFileStorage; + let s3clientAdapter: DeepMocked; + let repo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TemporaryFileStorage, + { + provide: TemporaryFileRepo, + useValue: createMock(), + }, + { provide: H5P_CONTENT_S3_CONNECTION, useValue: createMock() }, + ], + }).compile(); + storage = module.get(TemporaryFileStorage); + s3clientAdapter = module.get(H5P_CONTENT_S3_CONNECTION); + repo = module.get(TemporaryFileRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const fileContent = (userId: string, filename: string) => `Test content of ${userId}'s ${filename}`; + + const setup = () => { + const user1: Required = { + email: 'user1@example.org', + id: '12345-12345', + name: 'Marla Mathe', + type: 'local', + canCreateRestricted: false, + canInstallRecommended: false, + canUpdateAndInstallLibraries: false, + }; + const filename1 = 'abc/def.txt'; + const file1 = new H5pEditorTempFile({ + filename: filename1, + ownedByUserId: user1.id, + expiresAt: tomorrow, + birthtime: new Date(), + size: fileContent(user1.id, filename1).length, + }); + + const user2: Required = { + email: 'user2@example.org', + id: '54321-54321', + name: 'Mirjam Mathe', + type: 'local', + canCreateRestricted: false, + canInstallRecommended: false, + canUpdateAndInstallLibraries: false, + }; + const filename2 = 'uvw/xyz.txt'; + const file2 = new H5pEditorTempFile({ + filename: filename2, + ownedByUserId: user2.id, + expiresAt: tomorrow, + birthtime: new Date(), + size: fileContent(user2.id, filename2).length, + }); + + return { + user1, + user2, + file1, + file2, + }; + }; + + it('service should be defined', () => { + expect(storage).toBeDefined(); + }); + + describe('deleteFile is called', () => { + describe('WHEN file exists', () => { + it('should delete file', async () => { + const { user1, file1 } = setup(); + const res = [`h5p-tempfiles/${user1.id}/${file1.filename}`]; + repo.findByUserAndFilename.mockResolvedValueOnce(file1); + + await storage.deleteFile(file1.filename, user1.id); + + expect(repo.delete).toHaveBeenCalled(); + expect(s3clientAdapter.delete).toHaveBeenCalledTimes(1); + expect(s3clientAdapter.delete).toHaveBeenCalledWith(res); + }); + }); + describe('WHEN file does not exist', () => { + it('should throw error', async () => { + const { user1, file1 } = setup(); + repo.findByUserAndFilename.mockImplementation(() => { + throw new Error('Not found'); + }); + + await expect(async () => { + await storage.deleteFile(file1.filename, user1.id); + }).rejects.toThrow(); + + expect(repo.delete).not.toHaveBeenCalled(); + expect(s3clientAdapter.delete).not.toHaveBeenCalled(); + }); + }); + }); + + describe('fileExists is called', () => { + describe('WHEN file exists', () => { + it('should return true', async () => { + const { user1, file1 } = setup(); + repo.findByUserAndFilename.mockResolvedValueOnce(file1); + + const result = await storage.fileExists(file1.filename, user1); + + expect(result).toBe(true); + }); + }); + describe('WHEN file does not exist', () => { + it('should return false', async () => { + const { user1 } = setup(); + repo.findAllByUserAndFilename.mockResolvedValue([]); + + const exists = await storage.fileExists('abc/nonexistingfile.txt', user1); + + expect(exists).toBe(false); + }); + }); + }); + + describe('getFileStats is called', () => { + describe('WHEN file exists', () => { + it('should return file stats', async () => { + const { user1, file1 } = setup(); + repo.findByUserAndFilename.mockResolvedValueOnce(file1); + + const filestats = await storage.getFileStats(file1.filename, user1); + + expect(filestats.size).toBe(file1.size); + expect(filestats.birthtime).toBe(file1.birthtime); + }); + }); + describe('WHEN file does not exist', () => { + it('should throw error', async () => { + const { user1 } = setup(); + repo.findByUserAndFilename.mockImplementation(() => { + throw new Error('Not found'); + }); + + const fileStatsPromise = storage.getFileStats('abc/nonexistingfile.txt', user1); + + await expect(fileStatsPromise).rejects.toThrow(); + }); + }); + describe('WHEN filename is invalid', () => { + it('should throw error', async () => { + const { user1 } = setup(); + const fileStatsPromise = storage.getFileStats('/../&$!.txt', user1); + await expect(fileStatsPromise).rejects.toThrow(); + }); + }); + }); + + describe('getFileStream is called', () => { + describe('WHEN file exists and no range is given', () => { + it('should return readable file stream', async () => { + const { user1, file1 } = setup(); + const actualContent = fileContent(user1.id, file1.filename); + const response: Required = { + data: Readable.from(actualContent), + etag: '', + contentType: '', + contentLength: 0, + contentRange: '', + name: '', + }; + + repo.findByUserAndFilename.mockResolvedValueOnce(file1); + s3clientAdapter.get.mockResolvedValueOnce(response); + + const stream = await storage.getFileStream(file1.filename, user1); + + let content = Buffer.alloc(0); + await new Promise((resolve, reject) => { + stream.on('data', (chunk) => { + content += chunk; + }); + stream.on('error', reject); + stream.on('end', resolve); + }); + + expect(content).not.toBe(null); + expect(content.toString()).toEqual(actualContent); + }); + }); + describe('WHEN file does not exist', () => { + it('should throw error', async () => { + const { user1 } = setup(); + repo.findByUserAndFilename.mockImplementation(() => { + throw new Error('Not found'); + }); + + const fileStreamPromise = storage.getFileStream('abc/nonexistingfile.txt', user1); + + await expect(fileStreamPromise).rejects.toThrow(); + }); + }); + }); + + describe('listFiles is called', () => { + describe('WHEN existing user is given', () => { + it('should return only users file', async () => { + const { user1, file1 } = setup(); + repo.findByUser.mockResolvedValueOnce([file1]); + + const files = await storage.listFiles(user1); + + expect(files.length).toBe(1); + expect(files[0].ownedByUserId).toBe(user1.id); + expect(files[0].filename).toBe(file1.filename); + }); + }); + describe('WHEN no user is given', () => { + it('should return all expired files)', async () => { + const { user1, user2, file1, file2 } = setup(); + repo.findExpired.mockResolvedValueOnce([file1, file2]); + + const files = await storage.listFiles(); + + expect(files.length).toBe(2); + expect(files[0].ownedByUserId).toBe(user1.id); + expect(files[1].ownedByUserId).toBe(user2.id); + }); + }); + }); + describe('saveFile is called', () => { + describe('WHEN file exists', () => { + it('should overwrite file', async () => { + const { user1, file1 } = setup(); + const newData = 'This is new fake H5P content.'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const readStream = Readable.from(newData) as ReadStream; + repo.findByUserAndFilename.mockResolvedValueOnce(file1); + let savedData = Buffer.alloc(0); + s3clientAdapter.create.mockImplementation(async (path: string, file: File) => { + savedData += file.data.read(); + return Promise.resolve({} as ServiceOutputTypes); + }); + + await storage.saveFile(file1.filename, readStream, user1, tomorrow); + + expect(s3clientAdapter.delete).toHaveBeenCalled(); + expect(savedData.toString()).toBe(newData); + }); + }); + + describe('WHEN file does not exist', () => { + it('should create and overwrite new file', async () => { + const { user1 } = setup(); + const filename = 'newfile.txt'; + const newData = 'This is new fake H5P content.'; + const readStream = Readable.from(newData) as ReadStream; + let savedData = Buffer.alloc(0); + s3clientAdapter.create.mockImplementation(async (path: string, file: File) => { + savedData += file.data.read(); + return Promise.resolve({} as ServiceOutputTypes); + }); + + await storage.saveFile(filename, readStream, user1, tomorrow); + + expect(s3clientAdapter.delete).toHaveBeenCalled(); + expect(savedData.toString()).toBe(newData); + }); + }); + + describe('WHEN expirationTime is in the past', () => { + it('should throw error', async () => { + const { user1, file1 } = setup(); + const newData = 'This is new fake H5P content.'; + const readStream = Readable.from(newData) as ReadStream; + + const saveFile = storage.saveFile(file1.filename, readStream, user1, new Date(2023, 0, 1)); + + await expect(saveFile).rejects.toThrow(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts new file mode 100644 index 00000000000..6bfa164e630 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts @@ -0,0 +1,124 @@ +import { ITemporaryFile, ITemporaryFileStorage, IUser } from '@lumieducation/h5p-server'; +import { Inject, Injectable, NotAcceptableException } from '@nestjs/common'; +import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { ReadStream } from 'fs'; +import { Readable } from 'stream'; +import { H5pFileDto } from '../controller/dto/h5p-file.dto'; +import { H5pEditorTempFile } from '../entity/h5p-editor-tempfile.entity'; +import { H5P_CONTENT_S3_CONNECTION } from '../h5p-editor.config'; +import { TemporaryFileRepo } from '../repo/temporary-file.repo'; + +@Injectable() +export class TemporaryFileStorage implements ITemporaryFileStorage { + constructor( + private readonly repo: TemporaryFileRepo, + @Inject(H5P_CONTENT_S3_CONNECTION) private readonly s3Client: S3ClientAdapter + ) {} + + private checkFilename(filename: string): void { + if (!/^[a-zA-Z0-9/._-]+$/g.test(filename) && filename.includes('..') && filename.startsWith('/')) { + throw new NotAcceptableException(`Filename contains forbidden characters or is empty: '${filename}'`); + } + } + + private getFileInfo(filename: string, userId: string): Promise { + this.checkFilename(filename); + return this.repo.findByUserAndFilename(userId, filename); + } + + public async deleteFile(filename: string, userId: string): Promise { + this.checkFilename(filename); + const meta = await this.repo.findByUserAndFilename(userId, filename); + await this.s3Client.delete([this.getFilePath(userId, filename)]); + await this.repo.delete(meta); + } + + public async fileExists(filename: string, user: IUser): Promise { + this.checkFilename(filename); + const files = await this.repo.findAllByUserAndFilename(user.id, filename); + const exists = files.length !== 0; + return exists; + } + + public async getFileStats(filename: string, user: IUser): Promise { + return this.getFileInfo(filename, user.id); + } + + public async getFileStream( + filename: string, + user: IUser, + rangeStart = 0, + rangeEnd?: number | undefined + ): Promise { + this.checkFilename(filename); + const tempFile = await this.repo.findByUserAndFilename(user.id, filename); + const path = this.getFilePath(user.id, filename); + let rangeEndNew = 0; + if (rangeEnd === undefined) { + rangeEndNew = tempFile.size - 1; + } + const response = await this.s3Client.get(path, `${rangeStart}-${rangeEndNew}`); + + return response.data; + } + + public async listFiles(user?: IUser): Promise { + // method is expected to support listing all files in database + // Lumi uses the variant without a user to search for expired files, so we only return those + + let files: ITemporaryFile[]; + if (user) { + files = await this.repo.findByUser(user.id); + } else { + files = await this.repo.findExpired(); + } + + return files; + } + + public async saveFile( + filename: string, + dataStream: ReadStream, + user: IUser, + expirationTime: Date + ): Promise { + this.checkFilename(filename); + const now = new Date(); + if (expirationTime < now) { + throw new NotAcceptableException('expirationTime must be in the future'); + } + + const path = this.getFilePath(user.id, filename); + let tempFile: H5pEditorTempFile | undefined; + try { + tempFile = await this.repo.findByUserAndFilename(user.id, filename); + await this.s3Client.delete([path]); + } finally { + if (tempFile === undefined) { + tempFile = new H5pEditorTempFile({ + filename, + ownedByUserId: user.id, + expiresAt: expirationTime, + birthtime: new Date(), + size: dataStream.bytesRead, + }); + } else { + tempFile.expiresAt = expirationTime; + tempFile.size = dataStream.bytesRead; + } + } + await this.s3Client.create( + path, + new H5pFileDto({ name: path, mimeType: 'application/octet-stream', data: dataStream }) + ); + await this.repo.save(tempFile); + + return tempFile; + } + + private getFilePath(userId: string, filename: string): string { + const path = `h5p-tempfiles/${userId}/${filename}`; + + return path; + } +} diff --git a/apps/server/src/modules/h5p-editor/types/lumi-types.ts b/apps/server/src/modules/h5p-editor/types/lumi-types.ts new file mode 100644 index 00000000000..ed1aa36a21d --- /dev/null +++ b/apps/server/src/modules/h5p-editor/types/lumi-types.ts @@ -0,0 +1,45 @@ +import { IUser } from '@lumieducation/h5p-server'; +import { EntityId } from '@shared/domain'; +import { H5PContentParentType } from '../entity'; + +export interface H5PContentParentParams { + schoolId: EntityId; + parentType: H5PContentParentType; + parentId: EntityId; +} + +export class LumiUserWithContentData implements IUser { + contentParentType: H5PContentParentType; + + contentParentId: EntityId; + + schoolId: EntityId; + + canCreateRestricted: boolean; + + canInstallRecommended: boolean; + + canUpdateAndInstallLibraries: boolean; + + email: string; + + id: EntityId; + + name: string; + + type: 'local' | string; + + constructor(user: IUser, parentParams: H5PContentParentParams) { + this.contentParentType = parentParams.parentType; + this.contentParentId = parentParams.parentId; + this.schoolId = parentParams.schoolId; + + this.canCreateRestricted = user.canCreateRestricted; + this.canInstallRecommended = user.canInstallRecommended; + this.canUpdateAndInstallLibraries = user.canUpdateAndInstallLibraries; + this.email = user.email; + this.id = user.id; + this.name = user.name; + this.type = user.type; + } +} diff --git a/apps/server/src/modules/h5p-editor/uc/dto/h5p-getLibraryFile.ts b/apps/server/src/modules/h5p-editor/uc/dto/h5p-getLibraryFile.ts new file mode 100644 index 00000000000..2344b9efdbe --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/dto/h5p-getLibraryFile.ts @@ -0,0 +1,8 @@ +import { Readable } from 'stream'; + +export interface GetLibraryFile { + data: Readable; + contentType: string; + contentLength: number; + contentRange?: { start: number; end: number }; +} diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-ajax.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-ajax.uc.spec.ts new file mode 100644 index 00000000000..c42b959b9e8 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p-ajax.uc.spec.ts @@ -0,0 +1,227 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PAjaxEndpoint, H5PEditor, H5PPlayer, H5pError } from '@lumieducation/h5p-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LanguageType, UserDO } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { H5PErrorMapper } from '../mapper/h5p-error.mapper'; +import { H5PContentRepo } from '../repo'; +import { LibraryStorage } from '../service'; +import { H5PEditorUc } from './h5p.uc'; + +describe('H5P Ajax', () => { + let module: TestingModule; + let uc: H5PEditorUc; + let ajaxEndpoint: DeepMocked; + let userService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + H5PEditorUc, + { + provide: H5PEditor, + useValue: createMock(), + }, + { + provide: H5PPlayer, + useValue: createMock(), + }, + { + provide: LibraryStorage, + useValue: createMock(), + }, + { + provide: H5PAjaxEndpoint, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, + { + provide: H5PContentRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(H5PEditorUc); + ajaxEndpoint = module.get(H5PAjaxEndpoint); + userService = module.get(UserService); + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('when calling GET', () => { + const userMock = { + userId: 'dummyId', + roles: [], + schoolId: 'dummySchool', + accountId: 'dummyAccountId', + isExternalUser: false, + }; + const spy = jest.spyOn(H5PErrorMapper.prototype, 'mapH5pError'); + + it('should call H5PAjaxEndpoint.getAjax and return the result', async () => { + const dummyResponse = { + apiVersion: { major: 1, minor: 1 }, + details: [], + libraries: [], + outdated: false, + recentlyUsed: [], + user: 'DummyUser', + }; + + ajaxEndpoint.getAjax.mockResolvedValueOnce(dummyResponse); + userService.findById.mockResolvedValueOnce({ language: LanguageType.DE } as UserDO); + + const result = await uc.getAjax({ action: 'content-type-cache' }, userMock); + + expect(result).toBe(dummyResponse); + expect(ajaxEndpoint.getAjax).toHaveBeenCalledWith( + 'content-type-cache', + undefined, // MachineName + undefined, // MajorVersion + undefined, // MinorVersion + 'de', + expect.objectContaining({ id: 'dummyId' }) + ); + }); + + it('should invoce h5p-error mapper', async () => { + ajaxEndpoint.getAjax.mockRejectedValueOnce(new Error('Dummy Error')); + await uc.getAjax({ action: 'content-type-cache' }, userMock); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('when calling POST', () => { + const userMock = { + userId: 'dummyId', + roles: [], + schoolId: 'dummySchool', + accountId: 'dummyAccountId', + isExternalUser: false, + }; + const spy = jest.spyOn(H5PErrorMapper.prototype, 'mapH5pError'); + + it('should call H5PAjaxEndpoint.postAjax and return the result', async () => { + const dummyResponse = [ + { + majorVersion: 1, + minorVersion: 2, + metadataSettings: {}, + name: 'Dummy Library', + restricted: false, + runnable: true, + title: 'Dummy Library', + tutorialUrl: '', + uberName: 'dummyLibrary-1.1', + }, + ]; + + ajaxEndpoint.postAjax.mockResolvedValueOnce(dummyResponse); + + const result = await uc.postAjax( + userMock, + { action: 'libraries' }, + { contentId: 'id', field: 'field', libraries: ['dummyLibrary-1.0'], libraryParameters: '' } + ); + + expect(result).toBe(dummyResponse); + expect(ajaxEndpoint.postAjax).toHaveBeenCalledWith( + 'libraries', + { contentId: 'id', field: 'field', libraries: ['dummyLibrary-1.0'], libraryParameters: '' }, + 'de', + expect.objectContaining({ id: 'dummyId' }), + undefined, + undefined, + undefined, + undefined, + undefined + ); + }); + + it('should call H5PAjaxEndpoint.postAjax with files', async () => { + const dummyResponse = [ + { + majorVersion: 1, + minorVersion: 2, + metadataSettings: {}, + name: 'Dummy Library', + restricted: false, + runnable: true, + title: 'Dummy Library', + tutorialUrl: '', + uberName: 'dummyLibrary-1.1', + }, + ]; + + ajaxEndpoint.postAjax.mockResolvedValueOnce(dummyResponse); + + const result = await uc.postAjax( + userMock, + { action: 'libraries' }, + { contentId: 'id', field: 'field', libraries: ['dummyLibrary-1.0'], libraryParameters: '' }, + { + fieldname: 'file', + buffer: Buffer.from(''), + originalname: 'OriginalFile.jpg', + size: 0, + mimetype: 'image/jpg', + } as Express.Multer.File, + { + fieldname: 'h5p', + buffer: Buffer.from(''), + originalname: 'OriginalFile.jpg', + size: 0, + mimetype: 'image/jpg', + } as Express.Multer.File + ); + + const bufferTest = { + data: expect.any(Buffer), + mimetype: 'image/jpg', + name: 'OriginalFile.jpg', + size: 0, + }; + + expect(result).toBe(dummyResponse); + expect(ajaxEndpoint.postAjax).toHaveBeenCalledWith( + 'libraries', + { contentId: 'id', field: 'field', libraries: ['dummyLibrary-1.0'], libraryParameters: '' }, + 'de', + expect.objectContaining({ id: 'dummyId' }), + bufferTest, + undefined, + undefined, + bufferTest, + undefined + ); + }); + + it('should invoce h5p-error.mapper', async () => { + ajaxEndpoint.postAjax.mockRejectedValueOnce(new H5pError('dummy-error', { error: 'Dummy Error' }, 400)); + + await uc.postAjax( + userMock, + { action: 'libraries' }, + { contentId: 'id', field: 'field', libraries: ['dummyLibrary-1.0'], libraryParameters: '' } + ); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts new file mode 100644 index 00000000000..174d5c0fd3a --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts @@ -0,0 +1,188 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PEditor, H5PPlayer } from '@lumieducation/h5p-server'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { h5pContentFactory, setupEntities } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { H5PContentRepo } from '../repo'; +import { LibraryStorage } from '../service'; +import { H5PAjaxEndpointProvider } from '../provider'; +import { H5PEditorUc } from './h5p.uc'; + +const createParams = () => { + const content = h5pContentFactory.build(); + + const mockCurrentUser: ICurrentUser = { + accountId: 'mockAccountId', + roles: ['student'], + schoolId: 'mockSchoolId', + userId: 'mockUserId', + isExternalUser: false, + }; + + return { content, mockCurrentUser }; +}; + +describe('save or create H5P content', () => { + let module: TestingModule; + let uc: H5PEditorUc; + let h5pEditor: DeepMocked; + let h5pContentRepo: DeepMocked; + let authorizationReferenceService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + H5PEditorUc, + H5PAjaxEndpointProvider, + { + provide: H5PEditor, + useValue: createMock(), + }, + { + provide: H5PPlayer, + useValue: createMock(), + }, + { + provide: LibraryStorage, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, + { + provide: H5PContentRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(H5PEditorUc); + h5pEditor = module.get(H5PEditor); + h5pContentRepo = module.get(H5PContentRepo); + authorizationReferenceService = module.get(AuthorizationReferenceService); + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('deleteH5pContent is called', () => { + describe('WHEN user is authorized and service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + h5pEditor.deleteContent.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { content, mockCurrentUser }; + }; + + it('should call authorizationReferenceService.checkPermissionByReferences', async () => { + const { content, mockCurrentUser } = setup(); + + await uc.deleteH5pContent(mockCurrentUser, content.id); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + content.parentType, + content.parentId, + AuthorizationContextBuilder.write([]) + ); + }); + + it('should call service with correct params', async () => { + const { content, mockCurrentUser } = setup(); + + await uc.deleteH5pContent(mockCurrentUser, content.id); + + expect(h5pEditor.deleteContent).toBeCalledWith( + content.id, + expect.objectContaining({ + id: mockCurrentUser.userId, + }) + ); + }); + + it('should return true', async () => { + const { content, mockCurrentUser } = setup(); + + const result = await uc.deleteH5pContent(mockCurrentUser, content.id); + + expect(result).toBe(true); + }); + }); + + describe('WHEN content does not exist', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + h5pContentRepo.findById.mockRejectedValueOnce(new NotFoundException()); + + return { content, mockCurrentUser }; + }; + + it('should throw NotFoundException', async () => { + const { content, mockCurrentUser } = setup(); + + const deleteH5pContentpromise = uc.deleteH5pContent(mockCurrentUser, content.id); + + await expect(deleteH5pContentpromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN user is not authorized', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + + return { content, mockCurrentUser }; + }; + + it('should throw forbidden error', async () => { + const { content, mockCurrentUser } = setup(); + + const deleteH5pContentpromise = uc.deleteH5pContent(mockCurrentUser, content.id); + + await expect(deleteH5pContentpromise).rejects.toThrow(ForbiddenException); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + const error = new Error('test'); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + h5pEditor.deleteContent.mockRejectedValueOnce(error); + + return { error, content, mockCurrentUser }; + }; + + it('should return error of service', async () => { + const { content, mockCurrentUser } = setup(); + + const deleteH5pContentpromise = uc.deleteH5pContent(mockCurrentUser, content.id); + + await expect(deleteH5pContentpromise).rejects.toThrow(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts new file mode 100644 index 00000000000..ab38282cc56 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts @@ -0,0 +1,592 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PAjaxEndpoint, H5PEditor, IPlayerModel } from '@lumieducation/h5p-server'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { h5pContentFactory, setupEntities } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { Request } from 'express'; +import { Readable } from 'stream'; +import { H5PContentRepo } from '../repo'; +import { ContentStorage, LibraryStorage } from '../service'; +import { H5PEditorProvider, H5PPlayerProvider } from '../provider'; +import { TemporaryFileStorage } from '../service/temporary-file-storage.service'; +import { H5PEditorUc } from './h5p.uc'; + +const createParams = () => { + const content = h5pContentFactory.build(); + + const mockCurrentUser: ICurrentUser = { + accountId: 'mockAccountId', + roles: ['student'], + schoolId: 'mockSchoolId', + userId: 'mockUserId', + isExternalUser: false, + }; + + const mockContentParameters: Awaited> = { + h5p: content.metadata, + library: content.metadata.mainLibrary, + params: { + metadata: content.metadata, + params: content.content, + }, + }; + + const playerResponseMock = expect.objectContaining({ + contentId: content.id, + }) as IPlayerModel; + + return { content, mockCurrentUser, playerResponseMock, mockContentParameters }; +}; + +describe('H5P Files', () => { + let module: TestingModule; + let uc: H5PEditorUc; + let libraryStorage: DeepMocked; + let ajaxEndpointService: DeepMocked; + let h5pContentRepo: DeepMocked; + let authorizationReferenceService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + H5PEditorUc, + H5PEditorProvider, + H5PPlayerProvider, + { + provide: H5PAjaxEndpoint, + useValue: createMock(), + }, + { + provide: ContentStorage, + useValue: createMock(), + }, + { + provide: LibraryStorage, + useValue: createMock(), + }, + { + provide: TemporaryFileStorage, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, + { + provide: H5PContentRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(H5PEditorUc); + libraryStorage = module.get(LibraryStorage); + ajaxEndpointService = module.get(H5PAjaxEndpoint); + h5pContentRepo = module.get(H5PContentRepo); + authorizationReferenceService = module.get(AuthorizationReferenceService); + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getContentParameters is called', () => { + describe('WHEN user is authorized and service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + ajaxEndpointService.getContentParameters.mockResolvedValueOnce(mockContentParameters); + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { content, mockCurrentUser, mockContentParameters }; + }; + + it('should call authorizationService.checkPermissionByReferences', async () => { + const { content, mockCurrentUser } = setup(); + + await uc.getContentParameters(content.id, mockCurrentUser); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + content.parentType, + content.parentId, + AuthorizationContextBuilder.read([]) + ); + }); + + it('should call service with correct params', async () => { + const { content, mockCurrentUser } = setup(); + + await uc.getContentParameters(content.id, mockCurrentUser); + + expect(ajaxEndpointService.getContentParameters).toHaveBeenCalledWith( + content.id, + expect.objectContaining({ + id: mockCurrentUser.userId, + }) + ); + }); + + it('should return results of service', async () => { + const { mockCurrentUser, content, mockContentParameters } = setup(); + + const result = await uc.getContentParameters(content.id, mockCurrentUser); + + expect(result).toEqual(mockContentParameters); + }); + }); + + describe('WHEN content does not exist', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + h5pContentRepo.findById.mockRejectedValueOnce(new NotFoundException()); + + return { content, mockCurrentUser }; + }; + + it('should throw NotFoundException', async () => { + const { mockCurrentUser, content } = setup(); + + const getContentParametersPromise = uc.getContentParameters(content.id, mockCurrentUser); + + await expect(getContentParametersPromise).rejects.toThrow(new NotFoundException()); + }); + }); + + describe('WHEN user is not authorized', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + + return { content, mockCurrentUser }; + }; + + it('should throw forbidden error', async () => { + const { mockCurrentUser, content } = setup(); + + const getContentParametersPromise = uc.getContentParameters(content.id, mockCurrentUser); + + await expect(getContentParametersPromise).rejects.toThrow(new ForbiddenException()); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { content, mockCurrentUser } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + ajaxEndpointService.getContentParameters.mockRejectedValueOnce(new Error('test')); + + return { content, mockCurrentUser }; + }; + + it('should return NotFoundException', async () => { + const { mockCurrentUser, content } = setup(); + + const getContentParametersPromise = uc.getContentParameters(content.id, mockCurrentUser); + + await expect(getContentParametersPromise).rejects.toThrow(new NotFoundException()); + }); + }); + }); + + describe('getContentFile is called', () => { + describe('WHEN user is authorized and service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const fileResponseMock = createMock>>(); + const requestMock = createMock({ + range: () => undefined, + }); + // Mock partial implementation so that range callback gets called + ajaxEndpointService.getContentFile.mockImplementationOnce((contentId, filename, user, rangeCallback) => { + rangeCallback?.(100); + return Promise.resolve(fileResponseMock); + }); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + const filename = 'test/file.txt'; + + return { content, filename, fileResponseMock, requestMock, mockCurrentUser, mockContentParameters }; + }; + + it('should call authorizationService.checkPermissionByReferences', async () => { + const { content, filename, requestMock, mockCurrentUser } = setup(); + + await uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + content.parentType, + content.parentId, + AuthorizationContextBuilder.read([]) + ); + }); + + it('should call service with correct params', async () => { + const { content, mockCurrentUser, filename, requestMock } = setup(); + + await uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + expect(ajaxEndpointService.getContentFile).toHaveBeenCalledWith( + content.id, + filename, + expect.objectContaining({ + id: mockCurrentUser.userId, + }), + expect.any(Function) + ); + }); + + it('should return results of service', async () => { + const { mockCurrentUser, fileResponseMock, filename, requestMock, content } = setup(); + + const result = await uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + expect(result).toEqual({ + data: fileResponseMock.stream, + contentType: fileResponseMock.mimetype, + contentLength: fileResponseMock.stats.size, + contentRange: fileResponseMock.range, + }); + }); + }); + + describe('WHEN user is authorized and a range is requested', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const range = { start: 0, end: 100 }; + + const requestMock = createMock({ + // @ts-expect-error partial types cause error + range: () => [range], + }); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + ajaxEndpointService.getContentFile.mockImplementationOnce((contentId, filename, user, rangeCallback) => { + const parsedRange = rangeCallback?.(100); + if (!parsedRange) throw new Error('no range'); + return Promise.resolve({ + range: parsedRange, + mimetype: '', + stats: { birthtime: new Date(), size: 100 }, + stream: createMock(), + }); + }); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + const filename = 'test/file.txt'; + + return { range, content, filename, requestMock, mockCurrentUser, mockContentParameters }; + }; + + it('should return parsed range', async () => { + const { mockCurrentUser, range, content, filename, requestMock } = setup(); + + const result = await uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + expect(result.contentRange).toEqual(range); + }); + }); + + describe('WHEN user is authorized but content range is bad', () => { + const setup = (rangeResponse?: { start: number; end: number }[] | -1 | -2) => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const requestMock = createMock({ + // @ts-expect-error partial types cause error + range() { + return rangeResponse; + }, + }); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + ajaxEndpointService.getContentFile.mockImplementationOnce((contentId, filename, user, rangeCallback) => { + rangeCallback?.(100); + return createMock(); + }); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + const filename = 'test/file.txt'; + + return { content, filename, requestMock, mockCurrentUser, mockContentParameters }; + }; + + describe('WHEN content range is invalid', () => { + it('should throw NotFoundException', async () => { + const { mockCurrentUser, filename, requestMock, content } = setup(-2); + + const getContentFilePromise = uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + await expect(getContentFilePromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN content range is unsatisfiable', () => { + it('should throw NotFoundException', async () => { + const { mockCurrentUser, filename, requestMock, content } = setup(-1); + + const getContentFilePromise = uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + await expect(getContentFilePromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN content range is multipart', () => { + it('should throw NotFoundException', async () => { + const { mockCurrentUser, filename, requestMock, content } = setup([ + { start: 0, end: 1 }, + { start: 2, end: 3 }, + ]); + + const getContentFilePromise = uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + await expect(getContentFilePromise).rejects.toThrow(NotFoundException); + }); + }); + }); + + describe('WHEN user is authorized but content does not exist', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const requestMock = createMock(); + const fileResponseMock = createMock>>(); + + h5pContentRepo.findById.mockRejectedValueOnce(new NotFoundException()); + + const filename = 'test/file.txt'; + + return { content, filename, fileResponseMock, requestMock, mockCurrentUser, mockContentParameters }; + }; + + it('should return error of service', async () => { + const { mockCurrentUser, filename, requestMock, content } = setup(); + + const getContentFilePromise = uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + await expect(getContentFilePromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN user is authorized but service throws error', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const requestMock = createMock(); + const fileResponseMock = createMock>>(); + + ajaxEndpointService.getContentFile.mockRejectedValueOnce(new Error('test')); + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + const filename = 'test/file.txt'; + + return { content, filename, fileResponseMock, requestMock, mockCurrentUser, mockContentParameters }; + }; + + it('should return error of service', async () => { + const { mockCurrentUser, filename, requestMock, content } = setup(); + + const getContentFilePromise = uc.getContentFile(content.id, filename, requestMock, mockCurrentUser); + + await expect(getContentFilePromise).rejects.toThrow(NotFoundException); + }); + }); + }); + + describe('getLibraryFile is called', () => { + describe('WHEN service executes successfully', () => { + const setup = () => { + const fileResponseMock = createMock>>(); + + libraryStorage.getLibraryFile.mockResolvedValueOnce(fileResponseMock); + + const ubername = 'H5P.Test-1.0'; + const filename = 'test/file.txt'; + + return { ubername, filename, fileResponseMock }; + }; + + it('should call service with correct params', async () => { + const { ubername, filename } = setup(); + + await uc.getLibraryFile(ubername, filename); + + expect(libraryStorage.getLibraryFile).toHaveBeenCalledWith(ubername, filename); + }); + + it('should return results of service', async () => { + const { ubername, filename, fileResponseMock } = setup(); + + const result = await uc.getLibraryFile(ubername, filename); + + expect(result).toEqual({ + data: fileResponseMock.stream, + contentType: fileResponseMock.mimetype, + contentLength: fileResponseMock.size, + }); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + libraryStorage.getLibraryFile.mockRejectedValueOnce(new Error('test')); + + const ubername = 'H5P.Test-1.0'; + const filename = 'test/file.txt'; + + return { ubername, filename }; + }; + + it('should return NotFoundException', async () => { + const { ubername, filename } = setup(); + + const getLibraryFilePromise = uc.getLibraryFile(ubername, filename); + + await expect(getLibraryFilePromise).rejects.toThrow(NotFoundException); + }); + }); + }); + + describe('getTemporaryFile is called', () => { + describe('WHEN service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const requestMock = createMock(); + const fileResponseMock = createMock>>(); + + ajaxEndpointService.getTemporaryFile.mockResolvedValueOnce(fileResponseMock); + + const filename = 'test/file.txt'; + + return { content, filename, fileResponseMock, requestMock, mockCurrentUser, mockContentParameters }; + }; + + it('should call service with correct params', async () => { + const { mockCurrentUser, filename, requestMock } = setup(); + + await uc.getTemporaryFile(filename, requestMock, mockCurrentUser); + + expect(ajaxEndpointService.getTemporaryFile).toHaveBeenCalledWith( + filename, + expect.objectContaining({ + id: mockCurrentUser.userId, + }), + expect.any(Function) + ); + }); + + it('should return results of service', async () => { + const { mockCurrentUser, fileResponseMock, filename, requestMock } = setup(); + + const result = await uc.getTemporaryFile(filename, requestMock, mockCurrentUser); + + expect(result).toEqual({ + data: fileResponseMock.stream, + contentType: fileResponseMock.mimetype, + contentLength: fileResponseMock.stats.size, + contentRange: fileResponseMock.range, + }); + }); + }); + + describe('WHEN content range is bad', () => { + const setup = (rangeResponse?: { start: number; end: number }[] | -1 | -2) => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const requestMock = createMock({ + // @ts-expect-error partial types cause error + range() { + return rangeResponse; + }, + }); + + ajaxEndpointService.getTemporaryFile.mockImplementationOnce((filename, user, rangeCallback) => { + rangeCallback?.(100); + return createMock(); + }); + const filename = 'test/file.txt'; + + return { content, filename, requestMock, mockCurrentUser, mockContentParameters }; + }; + + describe('WHEN content range is invalid', () => { + it('should throw NotFoundException', async () => { + const { mockCurrentUser, filename, requestMock } = setup(-2); + + const getTemporaryFilePromise = uc.getTemporaryFile(filename, requestMock, mockCurrentUser); + + await expect(getTemporaryFilePromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN content range is unsatisfiable', () => { + it('should throw NotFoundException', async () => { + const { mockCurrentUser, filename, requestMock } = setup(-1); + + const getTemporaryFilePromise = uc.getTemporaryFile(filename, requestMock, mockCurrentUser); + + await expect(getTemporaryFilePromise).rejects.toThrow(NotFoundException); + }); + }); + + describe('WHEN content range is multipart', () => { + it('should throw NotFoundException', async () => { + const { mockCurrentUser, filename, requestMock } = setup([ + { start: 0, end: 1 }, + { start: 2, end: 3 }, + ]); + + const getTemporaryFilePromise = uc.getTemporaryFile(filename, requestMock, mockCurrentUser); + + await expect(getTemporaryFilePromise).rejects.toThrow(NotFoundException); + }); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { content, mockCurrentUser, mockContentParameters } = createParams(); + + const requestMock = createMock(); + + ajaxEndpointService.getTemporaryFile.mockRejectedValueOnce(new Error('test')); + + const filename = 'test/file.txt'; + + return { content, filename, requestMock, mockCurrentUser, mockContentParameters }; + }; + + it('should return error of service', async () => { + const { mockCurrentUser, filename, requestMock } = setup(); + + const getTemporaryFilePromise = uc.getTemporaryFile(filename, requestMock, mockCurrentUser); + + await expect(getTemporaryFilePromise).rejects.toThrow(NotFoundException); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts new file mode 100644 index 00000000000..4322dd06352 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts @@ -0,0 +1,278 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PEditor, H5PPlayer, IEditorModel } from '@lumieducation/h5p-server'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LanguageType } from '@shared/domain'; +import { UserRepo } from '@shared/repo'; +import { h5pContentFactory, setupEntities } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { H5PContentRepo } from '../repo'; +import { LibraryStorage } from '../service'; +import { H5PAjaxEndpointProvider } from '../provider'; +import { H5PEditorUc } from './h5p.uc'; + +const createParams = () => { + const content = h5pContentFactory.build(); + + const mockCurrentUser: ICurrentUser = { + accountId: 'mockAccountId', + roles: ['student'], + schoolId: 'mockSchoolId', + userId: 'mockUserId', + isExternalUser: false, + }; + + const editorResponseMock = { scripts: ['test.js'] } as IEditorModel; + const contentResponseMock: Awaited> = { + h5p: content.metadata, + library: content.metadata.mainLibrary, + params: { + metadata: content.metadata, + params: content.content, + }, + }; + + const language = LanguageType.DE; + + return { content, mockCurrentUser, editorResponseMock, contentResponseMock, language }; +}; + +describe('get H5P editor', () => { + let module: TestingModule; + let uc: H5PEditorUc; + let h5pEditor: DeepMocked; + let h5pContentRepo: DeepMocked; + let authorizationReferenceService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + H5PEditorUc, + H5PAjaxEndpointProvider, + { + provide: H5PEditor, + useValue: createMock(), + }, + { + provide: H5PPlayer, + useValue: createMock(), + }, + { + provide: UserRepo, + useValue: createMock(), + }, + { + provide: LibraryStorage, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, + { + provide: H5PContentRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(H5PEditorUc); + h5pEditor = module.get(H5PEditor); + h5pContentRepo = module.get(H5PContentRepo); + authorizationReferenceService = module.get(AuthorizationReferenceService); + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getEmptyH5pEditor is called', () => { + describe('WHEN service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser, editorResponseMock, language } = createParams(); + + h5pEditor.render.mockResolvedValueOnce(editorResponseMock); + + return { content, mockCurrentUser, editorResponseMock, language }; + }; + + it('should call service with correct params', async () => { + const { mockCurrentUser, language } = setup(); + + await uc.getEmptyH5pEditor(mockCurrentUser, language); + + expect(h5pEditor.render).toHaveBeenCalledWith( + undefined, + language, + expect.objectContaining({ + id: mockCurrentUser.userId, + }) + ); + }); + + it('should return results of service', async () => { + const { mockCurrentUser, language, editorResponseMock } = setup(); + + const result = await uc.getEmptyH5pEditor(mockCurrentUser, language); + + expect(result).toEqual(editorResponseMock); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { content, mockCurrentUser, editorResponseMock, language } = createParams(); + + const error = new Error('test'); + + h5pEditor.render.mockRejectedValueOnce(error); + + return { error, content, mockCurrentUser, editorResponseMock, language }; + }; + + it('should return error of service', async () => { + const { error, mockCurrentUser, language } = setup(); + + const getEmptyEditorPromise = uc.getEmptyH5pEditor(mockCurrentUser, language); + + await expect(getEmptyEditorPromise).rejects.toThrow(error); + }); + }); + }); + + describe('getH5pEditor is called', () => { + describe('WHEN user is authorized and service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser, editorResponseMock, contentResponseMock, language } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + h5pEditor.render.mockResolvedValueOnce(editorResponseMock); + h5pEditor.getContent.mockResolvedValueOnce(contentResponseMock); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { content, mockCurrentUser, editorResponseMock, contentResponseMock, language }; + }; + + it('should call authorizationService.checkPermissionByReferences', async () => { + const { content, language, mockCurrentUser } = setup(); + + await uc.getH5pEditor(mockCurrentUser, content.id, language); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + content.parentType, + content.parentId, + AuthorizationContextBuilder.write([]) + ); + }); + + it('should call service with correct params', async () => { + const { content, language, mockCurrentUser } = setup(); + + await uc.getH5pEditor(mockCurrentUser, content.id, language); + + expect(h5pEditor.render).toHaveBeenCalledWith( + content.id, + language, + expect.objectContaining({ + id: mockCurrentUser.userId, + }) + ); + expect(h5pEditor.getContent).toHaveBeenCalledWith( + content.id, + expect.objectContaining({ + id: mockCurrentUser.userId, + }) + ); + }); + + it('should return results of service', async () => { + const { content, language, mockCurrentUser, contentResponseMock, editorResponseMock } = setup(); + + const result = await uc.getH5pEditor(mockCurrentUser, content.id, language); + + expect(result).toEqual({ + content: contentResponseMock, + editorModel: editorResponseMock, + }); + }); + }); + + describe('WHEN content does not exist', () => { + const setup = () => { + const { content, mockCurrentUser, editorResponseMock, language } = createParams(); + + h5pContentRepo.findById.mockRejectedValueOnce(new NotFoundException()); + + return { content, mockCurrentUser, editorResponseMock, language }; + }; + + it('should throw NotFoundException', async () => { + const { content, mockCurrentUser, language } = setup(); + + const getEditorPromise = uc.getH5pEditor(mockCurrentUser, content.id, language); + + await expect(getEditorPromise).rejects.toThrow(new NotFoundException()); + + expect(h5pEditor.render).toHaveBeenCalledTimes(0); + expect(h5pEditor.getContent).toHaveBeenCalledTimes(0); + }); + }); + + describe('WHEN user is not authorized', () => { + const setup = () => { + const { content, mockCurrentUser, editorResponseMock, contentResponseMock, language } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + + return { content, mockCurrentUser, editorResponseMock, contentResponseMock, language }; + }; + + it('should throw forbidden error', async () => { + const { content, mockCurrentUser, language } = setup(); + + const getEditorPromise = uc.getH5pEditor(mockCurrentUser, content.id, language); + + await expect(getEditorPromise).rejects.toThrow(new ForbiddenException()); + + expect(h5pEditor.render).toHaveBeenCalledTimes(0); + expect(h5pEditor.getContent).toHaveBeenCalledTimes(0); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { content, mockCurrentUser, editorResponseMock, contentResponseMock, language } = createParams(); + + const error = new Error('test'); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + h5pEditor.render.mockRejectedValueOnce(error); + h5pEditor.getContent.mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { error, content, mockCurrentUser, editorResponseMock, contentResponseMock, language }; + }; + + it('should return error of service', async () => { + const { content, mockCurrentUser, language, error } = setup(); + + const getEditorPromise = uc.getH5pEditor(mockCurrentUser, content.id, language); + + await expect(getEditorPromise).rejects.toThrow(error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts new file mode 100644 index 00000000000..6db9d27a905 --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts @@ -0,0 +1,198 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PEditor, H5PPlayer, IPlayerModel } from '@lumieducation/h5p-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { h5pContentFactory, setupEntities } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { H5PContentRepo } from '../repo'; +import { LibraryStorage } from '../service'; +import { H5PAjaxEndpointProvider } from '../provider'; +import { H5PEditorUc } from './h5p.uc'; + +const createParams = () => { + const content = h5pContentFactory.build(); + + const mockCurrentUser: ICurrentUser = { + accountId: 'mockAccountId', + roles: ['student'], + schoolId: 'mockSchoolId', + userId: 'mockUserId', + isExternalUser: false, + }; + + const playerResponseMock = expect.objectContaining({ + contentId: content.id, + }) as IPlayerModel; + + return { content, mockCurrentUser, playerResponseMock }; +}; + +describe('get H5P player', () => { + let module: TestingModule; + let uc: H5PEditorUc; + let h5pPlayer: DeepMocked; + let h5pContentRepo: DeepMocked; + let authorizationReferenceService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + H5PEditorUc, + H5PAjaxEndpointProvider, + { + provide: H5PEditor, + useValue: createMock(), + }, + { + provide: H5PPlayer, + useValue: createMock(), + }, + { + provide: LibraryStorage, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, + { + provide: H5PContentRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(H5PEditorUc); + h5pPlayer = module.get(H5PPlayer); + h5pContentRepo = module.get(H5PContentRepo); + authorizationReferenceService = module.get(AuthorizationReferenceService); + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getH5pPlayer is called', () => { + describe('WHEN user is authorized and service executes successfully', () => { + const setup = () => { + const { content, mockCurrentUser, playerResponseMock } = createParams(); + + const expectedResult = playerResponseMock; + + h5pContentRepo.findById.mockResolvedValueOnce(content); + h5pPlayer.render.mockResolvedValueOnce(expectedResult); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { content, mockCurrentUser, expectedResult }; + }; + + it('should call authorizationService.checkPermissionByReferences', async () => { + const { content, mockCurrentUser } = setup(); + + await uc.getH5pPlayer(mockCurrentUser, content.id); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + content.parentType, + content.parentId, + AuthorizationContextBuilder.read([]) + ); + }); + + it('should call service with correct params', async () => { + const { content, mockCurrentUser } = setup(); + + await uc.getH5pPlayer(mockCurrentUser, content.id); + + expect(h5pPlayer.render).toHaveBeenCalledWith( + content.id, + expect.objectContaining({ + id: mockCurrentUser.userId, + }) + ); + }); + + it('should return results of service', async () => { + const { content, mockCurrentUser, expectedResult } = setup(); + + const result = await uc.getH5pPlayer(mockCurrentUser, content.id); + + expect(result).toEqual(expectedResult); + }); + }); + }); + + describe('WHEN content does not exist', () => { + const setup = () => { + const { content, mockCurrentUser, playerResponseMock } = createParams(); + + h5pContentRepo.findById.mockRejectedValueOnce(new NotFoundException()); + + return { content, mockCurrentUser, playerResponseMock }; + }; + + it('should throw NotFoundException', async () => { + const { content, mockCurrentUser } = setup(); + + const getPlayerPromise = uc.getH5pPlayer(mockCurrentUser, content.id); + + await expect(getPlayerPromise).rejects.toThrow(new NotFoundException()); + + expect(h5pPlayer.render).toHaveBeenCalledTimes(0); + }); + }); + + describe('WHEN user is not authorized', () => { + const setup = () => { + const { content, mockCurrentUser, playerResponseMock } = createParams(); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + + return { content, mockCurrentUser, playerResponseMock }; + }; + + it('should throw forbidden error', async () => { + const { content, mockCurrentUser } = setup(); + + const getPlayerPromise = uc.getH5pPlayer(mockCurrentUser, content.id); + + await expect(getPlayerPromise).rejects.toThrow(new ForbiddenException()); + + expect(h5pPlayer.render).toHaveBeenCalledTimes(0); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { content, mockCurrentUser, playerResponseMock } = createParams(); + + const error = new Error('test'); + + h5pContentRepo.findById.mockResolvedValueOnce(content); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + h5pPlayer.render.mockRejectedValueOnce(error); + + return { error, content, mockCurrentUser, playerResponseMock }; + }; + + it('should return error of service', async () => { + const { error, content, mockCurrentUser } = setup(); + + const getPlayerPromise = uc.getH5pPlayer(mockCurrentUser, content.id); + + await expect(getPlayerPromise).rejects.toThrow(error); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts new file mode 100644 index 00000000000..2bd23edecdc --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts @@ -0,0 +1,340 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { H5PEditor, H5PPlayer } from '@lumieducation/h5p-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { h5pContentFactory, setupEntities } from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ForbiddenException } from '@nestjs/common'; +import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { LibraryStorage } from '../service'; +import { H5PAjaxEndpointProvider } from '../provider'; +import { H5PEditorUc } from './h5p.uc'; +import { H5PContentParentType } from '../entity'; +import { H5PContentRepo } from '../repo'; +import { LumiUserWithContentData } from '../types/lumi-types'; + +const createParams = () => { + const { content: parameters, metadata } = h5pContentFactory.build(); + + const mainLibraryUbername = metadata.mainLibrary; + + const contentId = new ObjectId().toHexString(); + const parentId = new ObjectId().toHexString(); + + const mockCurrentUser: ICurrentUser = { + accountId: 'mockAccountId', + roles: ['student'], + schoolId: 'mockSchoolId', + userId: 'mockUserId', + isExternalUser: false, + }; + + return { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser }; +}; + +describe('save or create H5P content', () => { + let module: TestingModule; + let uc: H5PEditorUc; + let h5pEditor: DeepMocked; + let authorizationReferenceService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + H5PEditorUc, + H5PAjaxEndpointProvider, + { + provide: H5PEditor, + useValue: createMock(), + }, + { + provide: H5PPlayer, + useValue: createMock(), + }, + { + provide: LibraryStorage, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, + { + provide: H5PContentRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(H5PEditorUc); + h5pEditor = module.get(H5PEditor); + authorizationReferenceService = module.get(AuthorizationReferenceService); + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('saveH5pContentGetMetadata is called', () => { + describe('WHEN user is authorized and service saves successfully', () => { + const setup = () => { + const { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser } = createParams(); + + h5pEditor.saveOrUpdateContentReturnMetaData.mockResolvedValueOnce({ id: contentId, metadata }); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { contentId, parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId }; + }; + + it('should call authorizationService.checkPermissionByReferences', async () => { + const { contentId, parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId } = setup(); + + await uc.saveH5pContentGetMetadata( + contentId, + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + H5PContentParentType.Lesson, + parentId, + AuthorizationContextBuilder.write([]) + ); + }); + + it('should call service with correct params', async () => { + const { contentId, parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId } = setup(); + + await uc.saveH5pContentGetMetadata( + contentId, + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + expect(h5pEditor.saveOrUpdateContentReturnMetaData).toHaveBeenCalledWith( + contentId, + parameters, + metadata, + mainLibraryUbername, + expect.any(LumiUserWithContentData) + ); + }); + + it('should return results of service', async () => { + const { contentId, parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId } = setup(); + + const result = await uc.saveH5pContentGetMetadata( + contentId, + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + expect(result).toEqual({ id: contentId, metadata }); + }); + }); + + describe('WHEN user is not authorized', () => { + const setup = () => { + const { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser } = createParams(); + + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + + return { contentId, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId }; + }; + + it('should throw forbidden error', async () => { + const { contentId, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId } = setup(); + + const saveContentPromise = uc.saveH5pContentGetMetadata( + contentId, + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + await expect(saveContentPromise).rejects.toThrow(new ForbiddenException()); + + expect(h5pEditor.saveOrUpdateContentReturnMetaData).toHaveBeenCalledTimes(0); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser } = createParams(); + + const error = new Error('test'); + + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + h5pEditor.saveOrUpdateContentReturnMetaData.mockRejectedValueOnce(error); + + return { error, contentId, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId }; + }; + + it('should return error of service', async () => { + const { error, contentId, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId } = setup(); + + const saveContentPromise = uc.saveH5pContentGetMetadata( + contentId, + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + await expect(saveContentPromise).rejects.toThrow(error); + }); + }); + }); + + describe('createH5pContentGetMetadata is called', () => { + describe('WHEN user is authorized and service creates successfully', () => { + const setup = () => { + const { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser } = createParams(); + + h5pEditor.saveOrUpdateContentReturnMetaData.mockResolvedValueOnce({ id: contentId, metadata }); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + + return { contentId, parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId }; + }; + + it('should call authorizationService.checkPermissionByReferences', async () => { + const { parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId } = setup(); + + await uc.createH5pContentGetMetadata( + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( + mockCurrentUser.userId, + H5PContentParentType.Lesson, + parentId, + AuthorizationContextBuilder.write([]) + ); + }); + + it('should call service with correct params', async () => { + const { parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId } = setup(); + + await uc.createH5pContentGetMetadata( + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + expect(h5pEditor.saveOrUpdateContentReturnMetaData).toHaveBeenCalledWith( + undefined, + parameters, + metadata, + mainLibraryUbername, + expect.any(LumiUserWithContentData) + ); + }); + + it('should return results of service', async () => { + const { contentId, parameters, metadata, mainLibraryUbername, mockCurrentUser, parentId } = setup(); + + const result = await uc.createH5pContentGetMetadata( + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + expect(result).toEqual({ id: contentId, metadata }); + }); + }); + + describe('WHEN user is not authorized', () => { + const setup = () => { + const { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser } = createParams(); + + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + + return { contentId, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId }; + }; + + it('should throw forbidden error', async () => { + const { mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId } = setup(); + + const saveContentPromise = uc.createH5pContentGetMetadata( + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + await expect(saveContentPromise).rejects.toThrow(new ForbiddenException()); + + expect(h5pEditor.saveOrUpdateContentReturnMetaData).toHaveBeenCalledTimes(0); + }); + }); + + describe('WHEN service throws error', () => { + const setup = () => { + const { contentId, parameters, metadata, mainLibraryUbername, parentId, mockCurrentUser } = createParams(); + + const error = new Error('test'); + + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); + h5pEditor.saveOrUpdateContentReturnMetaData.mockRejectedValueOnce(error); + + return { error, contentId, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId }; + }; + + it('should return error of service', async () => { + const { error, mockCurrentUser, parameters, metadata, mainLibraryUbername, parentId } = setup(); + + const saveContentPromise = uc.createH5pContentGetMetadata( + mockCurrentUser, + parameters, + metadata, + mainLibraryUbername, + H5PContentParentType.Lesson, + parentId + ); + + await expect(saveContentPromise).rejects.toThrow(error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/h5p-editor/uc/h5p.uc.ts b/apps/server/src/modules/h5p-editor/uc/h5p.uc.ts new file mode 100644 index 00000000000..f456491509a --- /dev/null +++ b/apps/server/src/modules/h5p-editor/uc/h5p.uc.ts @@ -0,0 +1,410 @@ +import { + AjaxSuccessResponse, + H5PAjaxEndpoint, + H5PEditor, + H5PPlayer, + IContentMetadata, + IEditorModel, + IPlayerModel, + IUser as LumiIUser, +} from '@lumieducation/h5p-server'; +import { + IAjaxResponse, + IHubInfo, + ILibraryDetailedDataForClient, + ILibraryOverviewForClient, +} from '@lumieducation/h5p-server/build/src/types'; +import { + BadRequestException, + HttpException, + Injectable, + NotAcceptableException, + NotFoundException, +} from '@nestjs/common'; +import { EntityId, LanguageType } from '@shared/domain'; +import { ICurrentUser } from '@src/modules/authentication'; +import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@src/modules/authorization/domain'; +import { UserService } from '@src/modules/user'; +import { Request } from 'express'; +import { AjaxGetQueryParams, AjaxPostBodyParams, AjaxPostQueryParams } from '../controller/dto'; +import { H5PContentParentType } from '../entity'; +import { H5PContentMapper } from '../mapper/h5p-content.mapper'; +import { H5PErrorMapper } from '../mapper/h5p-error.mapper'; +import { H5PContentRepo } from '../repo'; +import { LibraryStorage } from '../service'; +import { LumiUserWithContentData } from '../types/lumi-types'; +import { GetLibraryFile } from './dto/h5p-getLibraryFile'; + +@Injectable() +export class H5PEditorUc { + constructor( + private readonly h5pEditor: H5PEditor, + private readonly h5pPlayer: H5PPlayer, + private readonly h5pAjaxEndpoint: H5PAjaxEndpoint, + private readonly libraryService: LibraryStorage, + private readonly userService: UserService, + private readonly authorizationReferenceService: AuthorizationReferenceService, + private readonly h5pContentRepo: H5PContentRepo + ) {} + + private async checkContentPermission( + userId: EntityId, + parentType: H5PContentParentType, + parentId: EntityId, + context: AuthorizationContext + ) { + const allowedType = H5PContentMapper.mapToAllowedAuthorizationEntityType(parentType); + await this.authorizationReferenceService.checkPermissionByReferences(userId, allowedType, parentId, context); + } + + private fakeUndefinedAsString = () => { + const value = undefined as unknown as string; + return value; + }; + + /** + * Returns a callback that parses the request range. + */ + private getRange(req: Request) { + return (filesize: number) => { + const range = req.range(filesize); + + if (range) { + if (range === -2) { + throw new BadRequestException('invalid range'); + } + + if (range === -1) { + throw new BadRequestException('unsatisfiable range'); + } + + if (range.length > 1) { + throw new BadRequestException('multipart ranges are unsupported'); + } + + return range[0]; + } + + return undefined; + }; + } + + public async getAjax( + query: AjaxGetQueryParams, + currentUser: ICurrentUser + ): Promise { + const user = this.changeUserType(currentUser); + const language = await this.getUserLanguage(currentUser); + const h5pErrorMapper = new H5PErrorMapper(); + + try { + const result = await this.h5pAjaxEndpoint.getAjax( + query.action, + query.machineName, + query.majorVersion, + query.minorVersion, + language, + user + ); + return result; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + h5pErrorMapper.mapH5pError(err); + return undefined; + } + } + + public async postAjax( + currentUser: ICurrentUser, + query: AjaxPostQueryParams, + body: AjaxPostBodyParams, + contentFile?: Express.Multer.File, + h5pFile?: Express.Multer.File + ): Promise< + | AjaxSuccessResponse + | { + height?: number; + mime: string; + path: string; + width?: number; + } + | ILibraryOverviewForClient[] + | undefined + > { + const user = this.changeUserType(currentUser); + const language = await this.getUserLanguage(currentUser); + const h5pErrorMapper = new H5PErrorMapper(); + + try { + const result = await this.h5pAjaxEndpoint.postAjax( + query.action, + body, + language, + user, + contentFile && { + data: contentFile.buffer, + mimetype: contentFile.mimetype, + name: contentFile.originalname, + size: contentFile.size, + }, + query.id, + undefined, + h5pFile && { + data: h5pFile.buffer, + mimetype: h5pFile.mimetype, + name: h5pFile.originalname, + size: h5pFile.size, + }, + undefined // TODO: HubID? + ); + + return result; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + h5pErrorMapper.mapH5pError(err); + return undefined; + } + } + + public async getContentParameters(contentId: string, currentUser: ICurrentUser) { + const { parentType, parentId } = await this.h5pContentRepo.findById(contentId); + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.read([])); + + const user = this.changeUserType(currentUser); + + try { + const result = await this.h5pAjaxEndpoint.getContentParameters(contentId, user); + + return result; + } catch (err) { + throw new NotFoundException(); + } + } + + public async getContentFile( + contentId: string, + file: string, + req: Request, + currentUser: ICurrentUser + ): Promise { + const { parentType, parentId } = await this.h5pContentRepo.findById(contentId); + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.read([])); + + const user = this.changeUserType(currentUser); + + try { + const rangeCallback = this.getRange(req); + const { mimetype, range, stats, stream } = await this.h5pAjaxEndpoint.getContentFile( + contentId, + file, + user, + rangeCallback + ); + + return { + data: stream, + contentType: mimetype, + contentLength: stats.size, + contentRange: range, // Range can be undefined, typings from @lumieducation/h5p-server are wrong + }; + } catch (err) { + throw new NotFoundException(); + } + } + + public async getLibraryFile(ubername: string, file: string): Promise { + try { + const { mimetype, size, stream } = await this.libraryService.getLibraryFile(ubername, file); + + return { + data: stream, + contentType: mimetype, + contentLength: size as number, + }; + } catch (err) { + throw new NotFoundException(); + } + } + + public async getTemporaryFile(file: string, req: Request, currentUser: ICurrentUser): Promise { + const user = this.changeUserType(currentUser); + + try { + const rangeCallback = this.getRange(req); + const adapterRangeCallback: (filesize: number) => { end: number; start: number } = (filesize) => { + let returnValue = { start: 0, end: 0 }; + + if (rangeCallback) { + const result = rangeCallback(filesize); + + if (result) { + returnValue = { start: result.start, end: result.end }; + } + } + + return returnValue; + }; + const { mimetype, range, stats, stream } = await this.h5pAjaxEndpoint.getTemporaryFile( + file, + user, + adapterRangeCallback + ); + + return { + data: stream, + contentType: mimetype, + contentLength: stats.size, + contentRange: range, // Range can be undefined, typings from @lumieducation/h5p-server are wrong + }; + } catch (err) { + throw new NotFoundException(); + } + } + + public async getH5pPlayer(currentUser: ICurrentUser, contentId: string): Promise { + const { parentType, parentId } = await this.h5pContentRepo.findById(contentId); + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.read([])); + + const user = this.changeUserType(currentUser); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const playerModel: IPlayerModel = await this.h5pPlayer.render(contentId, user); + + return playerModel; + } + + public async getEmptyH5pEditor(currentUser: ICurrentUser, language: LanguageType) { + const user = this.changeUserType(currentUser); + const fakeUndefinedString = this.fakeUndefinedAsString(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const createdH5PEditor: IEditorModel = await this.h5pEditor.render( + fakeUndefinedString, // Lumi typings are wrong because they dont "use strict", this method actually accepts both string and undefined + language, + user + ); + + return createdH5PEditor; + } + + public async getH5pEditor(currentUser: ICurrentUser, contentId: string, language: LanguageType) { + const { parentType, parentId } = await this.h5pContentRepo.findById(contentId); + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.write([])); + + const user = this.changeUserType(currentUser); + + const [editorModel, content] = await Promise.all([ + this.h5pEditor.render(contentId, language, user) as Promise, + this.h5pEditor.getContent(contentId, user), + ]); + + return { + editorModel, + content, + }; + } + + public async deleteH5pContent(currentUser: ICurrentUser, contentId: string): Promise { + const { parentType, parentId } = await this.h5pContentRepo.findById(contentId); + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.write([])); + + const user = this.changeUserType(currentUser); + let deletedContent = false; + try { + await this.h5pEditor.deleteContent(contentId, user); + deletedContent = true; + } catch (error) { + deletedContent = false; + throw new HttpException('message', 400, { + cause: new NotAcceptableException(error as string, 'content not found'), + }); + } + + return deletedContent; + } + + public async createH5pContentGetMetadata( + currentUser: ICurrentUser, + params: unknown, + metadata: IContentMetadata, + mainLibraryUbername: string, + parentType: H5PContentParentType, + parentId: EntityId + ): Promise<{ id: string; metadata: IContentMetadata }> { + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.write([])); + + const user = this.createAugmentedLumiUser(currentUser, parentType, parentId); + const fakeAsString = this.fakeUndefinedAsString(); + + const newContentId = await this.h5pEditor.saveOrUpdateContentReturnMetaData( + fakeAsString, // Lumi typings are wrong because they dont "use strict", this method actually accepts both string and undefined + params, + metadata, + mainLibraryUbername, + user + ); + + return newContentId; + } + + public async saveH5pContentGetMetadata( + contentId: string, + currentUser: ICurrentUser, + params: unknown, + metadata: IContentMetadata, + mainLibraryUbername: string, + parentType: H5PContentParentType, + parentId: EntityId + ): Promise<{ id: string; metadata: IContentMetadata }> { + await this.checkContentPermission(currentUser.userId, parentType, parentId, AuthorizationContextBuilder.write([])); + + const user = this.createAugmentedLumiUser(currentUser, parentType, parentId); + + const newContentId = await this.h5pEditor.saveOrUpdateContentReturnMetaData( + contentId, + params, + metadata, + mainLibraryUbername, + user + ); + + return newContentId; + } + + private changeUserType(currentUser: ICurrentUser): LumiIUser { + const user: LumiIUser = { + canCreateRestricted: false, + canInstallRecommended: true, + canUpdateAndInstallLibraries: true, + email: '', + id: currentUser.userId, + name: '', + type: '', + }; + + return user; + } + + private createAugmentedLumiUser( + currentUser: ICurrentUser, + contentParentType: H5PContentParentType, + contentParentId: EntityId + ) { + const user = new LumiUserWithContentData(this.changeUserType(currentUser), { + parentType: contentParentType, + parentId: contentParentId, + schoolId: currentUser.schoolId, + }); + + return user; + } + + private async getUserLanguage(currentUser: ICurrentUser): Promise { + const languageUser = await this.userService.findById(currentUser.userId); + let userLanguage = LanguageType.DE; + if (languageUser?.language) { + userLanguage = languageUser.language; + } + return userLanguage; + } +} diff --git a/apps/server/src/shared/infra/s3-client/interface/index.ts b/apps/server/src/shared/infra/s3-client/interface/index.ts index ad6ed9c81da..d3438099858 100644 --- a/apps/server/src/shared/infra/s3-client/interface/index.ts +++ b/apps/server/src/shared/infra/s3-client/interface/index.ts @@ -26,3 +26,17 @@ export interface File { data: Readable; mimeType: string; } + +export interface ListFiles { + path: string; + maxKeys?: number; + nextMarker?: string; + files?: string[]; +} + +export interface ObjectKeysRecursive { + path: string; + maxKeys: number | undefined; + nextMarker: string | undefined; + files: string[]; +} diff --git a/apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts b/apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts index 957d841f1fe..87de7afb1c3 100644 --- a/apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts +++ b/apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts @@ -1,7 +1,7 @@ import { S3Client, S3ServiceException } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { HttpException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ErrorUtils } from '@src/core/error/utils'; import { LegacyLogger } from '@src/core/logger'; @@ -564,4 +564,217 @@ describe('S3ClientAdapter', () => { await expect(service.copy(undefined)).rejects.toThrowError(InternalServerErrorException); }); }); + + describe('head', () => { + const setup = () => { + const { pathToFile } = createParameter(); + + return { pathToFile }; + }; + + describe('when file exists', () => { + it('should call send() of client with head object', async () => { + const { pathToFile } = setup(); + + await service.head(pathToFile); + + expect(client.send).toBeCalledWith( + expect.objectContaining({ + input: { Bucket: 'test-bucket', Key: pathToFile }, + }) + ); + }); + }); + + describe('when file does not exist', () => { + it('should throw HttpException', async () => { + const { pathToFile } = setup(); + // @ts-expect-error ignore parameter type of mock function + client.send.mockRejectedValueOnce(new Error('NoSuchKey')); + + const headPromise = service.head(pathToFile); + + await expect(headPromise).rejects.toBeInstanceOf(HttpException); + }); + }); + describe('when file exist and failed', () => { + it('should throw InternalServerErrorException', async () => { + const { pathToFile } = setup(); + // @ts-expect-error ignore parameter type of mock function + client.send.mockRejectedValueOnce(new Error('Dummy')); + + const headPromise = service.head(pathToFile); + + await expect(headPromise).rejects.toBeInstanceOf(InternalServerErrorException); + }); + }); + }); + + describe('list', () => { + const setup = () => { + const path = 'test/'; + + const keys = Array.from(Array(2500).keys()).map((n) => `KEY-${n}`); + const responseContents = keys.map((key) => { + return { + Key: `${path}${key}`, + }; + }); + + return { path, keys, responseContents }; + }; + + afterEach(() => { + client.send.mockClear(); + }); + + describe('when maxKeys is given', () => { + it('should truncate result', async () => { + const { path, keys, responseContents } = setup(); + + // @ts-expect-error ignore parameter type of mock function + client.send.mockResolvedValue({ + IsTruncated: false, + Contents: responseContents.slice(0, 500), + }); + + const resultKeys = await service.list({ path, maxKeys: 500 }); + + expect(resultKeys.files).toEqual(keys.slice(0, 500)); + + expect(client.send).toBeCalledWith( + expect.objectContaining({ + input: { + Bucket: 'test-bucket', + Prefix: path, + ContinuationToken: undefined, + MaxKeys: 500, + }, + }) + ); + }); + + it('should truncate result by S3 limits', async () => { + const { path, keys, responseContents } = setup(); + + // @ts-expect-error ignore parameter type of mock function + client.send.mockResolvedValueOnce({ + IsTruncated: true, + Contents: responseContents.slice(0, 1000), + ContinuationToken: 'KEY-1000', + }); + + // @ts-expect-error ignore parameter type of mock function + client.send.mockResolvedValueOnce({ + IsTruncated: true, + Contents: responseContents.slice(1000, 1200), + ContinuationToken: 'KEY-1200', + }); + + const resultKeys = await service.list({ path, maxKeys: 1200 }); + + expect(resultKeys.files).toEqual(keys.slice(0, 1200)); + + expect(client.send).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + input: { + Bucket: 'test-bucket', + Prefix: path, + ContinuationToken: undefined, + MaxKeys: 1200, + }, + }) + ); + + expect(client.send).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + input: { + Bucket: 'test-bucket', + Prefix: path, + ContinuationToken: 'KEY-1000', + MaxKeys: 200, + }, + }) + ); + + expect(client.send).toHaveBeenCalledTimes(2); + }); + }); + + describe('when maxKeys is not given', () => { + it('should call send() multiple times if bucket contains more than 1000 keys', async () => { + const { path, responseContents, keys } = setup(); + + client.send + // @ts-expect-error ignore parameter type of mock function + .mockResolvedValueOnce({ + IsTruncated: true, + ContinuationToken: '1', + Contents: responseContents.slice(0, 1000), + }) + // @ts-expect-error ignore parameter type of mock function + .mockResolvedValueOnce({ + IsTruncated: true, + ContinuationToken: '2', + Contents: responseContents.slice(1000, 2000), + }) + // @ts-expect-error ignore parameter type of mock function + .mockResolvedValueOnce({ + Contents: responseContents.slice(2000), + }); + + const resultKeys = await service.list({ path }); + + expect(resultKeys.files).toEqual(keys); + + expect(client.send).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + input: { + Bucket: 'test-bucket', + Prefix: path, + ContinuationToken: undefined, + }, + }) + ); + + expect(client.send).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + input: { + Bucket: 'test-bucket', + Prefix: path, + ContinuationToken: '1', + }, + }) + ); + + expect(client.send).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + input: { + Bucket: 'test-bucket', + Prefix: path, + ContinuationToken: '2', + }, + }) + ); + }); + }); + + describe('when client rejects with an error', () => { + it('should throw error', async () => { + const { path } = setup(); + + // @ts-expect-error ignore parameter type of mock function + client.send.mockRejectedValue(new Error()); + + const listPromise = service.list({ path }); + + await expect(listPromise).rejects.toThrow(); + }); + }); + }); }); diff --git a/apps/server/src/shared/infra/s3-client/s3-client.adapter.ts b/apps/server/src/shared/infra/s3-client/s3-client.adapter.ts index 1f4d47b5737..3c83fce7413 100644 --- a/apps/server/src/shared/infra/s3-client/s3-client.adapter.ts +++ b/apps/server/src/shared/infra/s3-client/s3-client.adapter.ts @@ -4,7 +4,9 @@ import { CreateBucketCommand, DeleteObjectsCommand, GetObjectCommand, - ListObjectsCommand, + HeadObjectCommand, + HeadObjectCommandOutput, + ListObjectsV2Command, S3Client, ServiceOutputTypes, } from '@aws-sdk/client-s3'; @@ -14,7 +16,7 @@ import { ErrorUtils } from '@src/core/error/utils'; import { LegacyLogger } from '@src/core/logger'; import { Readable } from 'stream'; import { S3_CLIENT, S3_CONFIG } from './constants'; -import { CopyFiles, File, GetFile, S3Config } from './interface'; +import { CopyFiles, File, GetFile, ListFiles, ObjectKeysRecursive, S3Config } from './interface'; @Injectable() export class S3ClientAdapter { @@ -196,11 +198,75 @@ export class S3ClientAdapter { } } + public async list(params: ListFiles): Promise { + try { + this.logger.log({ action: 'list', params }); + + const result = await this.listObjectKeysRecursive(params); + + return result; + } catch (err) { + throw new NotFoundException(null, ErrorUtils.createHttpExceptionOptions(err, 'S3ClientAdapter:listDirectory')); + } + } + + private async listObjectKeysRecursive(params: ListFiles): Promise { + const { path, maxKeys, nextMarker } = params; + let files: string[] = params.files ? params.files : []; + const MaxKeys = maxKeys && maxKeys - files.length; + + const req = new ListObjectsV2Command({ + Bucket: this.config.bucket, + Prefix: path, + ContinuationToken: nextMarker, + MaxKeys, + }); + + const data = await this.client.send(req); + + const returnedFiles = + data?.Contents?.filter((o) => o.Key) + .map((o) => o.Key as string) // Can not be undefined because of filter above + .map((key) => key.substring(path.length)) ?? []; + + files = files.concat(returnedFiles); + + let res: ObjectKeysRecursive = { path, maxKeys, nextMarker: data?.ContinuationToken, files }; + + if (data?.IsTruncated && (!maxKeys || res.files.length < maxKeys)) { + res = await this.listObjectKeysRecursive(res); + } + + return res; + } + + public async head(path: string): Promise { + try { + this.logger.log({ action: 'head', params: { path, bucket: this.config.bucket } }); + + const req = new HeadObjectCommand({ + Bucket: this.config.bucket, + Key: path, + }); + + const headResponse = await this.client.send(req); + + return headResponse; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (err.message && err.message === 'NoSuchKey') { + this.logger.log(`could not find the file for head with id ${path}`); + throw new NotFoundException(null, ErrorUtils.createHttpExceptionOptions(err, 'NoSuchKey')); + } + throw new InternalServerErrorException(null, ErrorUtils.createHttpExceptionOptions(err, 'S3ClientAdapter:head')); + } + } + public async deleteDirectory(path: string) { try { this.logger.log({ action: 'deleteDirectory', params: { path, bucket: this.config.bucket } }); - const req = new ListObjectsCommand({ + const req = new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: path, }); diff --git a/apps/server/src/shared/testing/factory/h5p-content.factory.ts b/apps/server/src/shared/testing/factory/h5p-content.factory.ts new file mode 100644 index 00000000000..4d07c369cd5 --- /dev/null +++ b/apps/server/src/shared/testing/factory/h5p-content.factory.ts @@ -0,0 +1,36 @@ +import { + ContentMetadata, + H5PContent, + H5PContentParentType, + IH5PContentProperties, +} from '@src/modules/h5p-editor/entity'; +import { ObjectID } from 'bson'; +import { BaseFactory } from './base.factory'; + +class H5PContentFactory extends BaseFactory {} + +export const h5pContentFactory = H5PContentFactory.define(H5PContent, ({ sequence }) => { + return { + parentType: H5PContentParentType.Lesson, + parentId: new ObjectID().toHexString(), + creatorId: new ObjectID().toHexString(), + schoolId: new ObjectID().toHexString(), + content: { + [`field${sequence}`]: sequence, + dateField: new Date(sequence), + thisObjectHasNoStructure: true, + nested: { + works: true, + }, + }, + metadata: new ContentMetadata({ + defaultLanguage: 'de-de', + embedTypes: ['iframe'], + language: 'de-de', + license: `License #${sequence}`, + mainLibrary: `Library-${sequence}.0`, + preloadedDependencies: [], + title: `Title #${sequence}`, + }), + }; +}); diff --git a/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts b/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts new file mode 100644 index 00000000000..4c9fbea5b11 --- /dev/null +++ b/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts @@ -0,0 +1,25 @@ +import { ITemporaryFileProperties, H5pEditorTempFile } from '@src/modules/h5p-editor/entity'; +import { DeepPartial } from 'fishery'; +import { BaseFactory } from './base.factory'; + +const oneDay = 24 * 60 * 60 * 1000; + +class H5PTemporaryFileFactory extends BaseFactory { + isExpired(): this { + const birthtime = new Date(Date.now() - oneDay * 2); // Created two days ago + const expiresAt = new Date(Date.now() - oneDay); // Expired yesterday + const params: DeepPartial = { expiresAt, birthtime }; + + return this.params(params); + } +} + +export const h5pTemporaryFileFactory = H5PTemporaryFileFactory.define(H5pEditorTempFile, ({ sequence }) => { + return { + filename: `File-${sequence}.txt`, + ownedByUserId: `user-${sequence}`, + birthtime: new Date(Date.now() - oneDay), // Yesterday + expiresAt: new Date(Date.now() + oneDay), // Tomorrow + size: sequence, + }; +}); diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index 7d5ec2ab753..bd8d0913a72 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -15,6 +15,8 @@ export * from './external-tool-pseudonym.factory'; export * from './federal-state.factory'; export * from './filerecord.factory'; export * from './group-entity.factory'; +export * from './h5p-content.factory'; +export * from './h5p-temporary-file.factory'; export * from './import-user.factory'; export * from './lesson.factory'; export * from './material.factory'; diff --git a/config/development.json b/config/development.json index 43d1b18640f..eb106993b10 100644 --- a/config/development.json +++ b/config/development.json @@ -31,6 +31,13 @@ "S3_ACCESS_KEY": "S3RVER", "S3_SECRET_KEY": "S3RVER" }, + "H5P_EDITOR": { + "S3_ENDPOINT": "http://localhost:5678", + "S3_REGION": "eu-central-1", + "S3_ACCESS_KEY_ID": "S3RVER", + "S3_SECRET_ACCESS_KEY": "S3RVER", + "S3_BUCKET_TEMP_FILES": "h5p-temp-files" + }, "FEATURE_IDENTITY_MANAGEMENT_ENABLED": true, "IDENTITY_MANAGEMENT": { "URI": "http://localhost:8080", diff --git a/package-lock.json b/package-lock.json index cd0606b9fde..5f895871520 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,8 @@ "freeport": "^1.0.5", "gm": "^1.25.0", "html-entities": "^2.3.2", + "i18next": "^23.3.0", + "i18next-fs-backend": "^2.1.5", "jose": "^1.28.1", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^2.0.5", @@ -2355,10 +2357,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", - "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", - "dev": true, + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -12737,6 +12738,33 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.3.0.tgz", + "integrity": "sha512-xd/UzWT71zYudCT7qVn6tB4yUVuXAhgCorsowYgM2EOdc14WqQBp5P2wEsxgfiDgdLN5XwJvTbzxrMfoY/nxnw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.22.5" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.5.tgz", + "integrity": "sha512-7fgSH8nVhXSBYPHR/W3tEXXhcnwHwNiND4Dfx9knzPzdsWTUTL/TdDVV+DY0dL0asHKLbdoJaXS4LdVW6R8MVQ==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -20580,8 +20608,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regexp-clone": { "version": "1.0.0", @@ -26659,10 +26686,9 @@ } }, "@babel/runtime": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", - "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", - "dev": true, + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", "requires": { "regenerator-runtime": "^0.13.11" } @@ -34395,6 +34421,19 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "i18next": { + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.3.0.tgz", + "integrity": "sha512-xd/UzWT71zYudCT7qVn6tB4yUVuXAhgCorsowYgM2EOdc14WqQBp5P2wEsxgfiDgdLN5XwJvTbzxrMfoY/nxnw==", + "requires": { + "@babel/runtime": "^7.22.5" + } + }, + "i18next-fs-backend": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.5.tgz", + "integrity": "sha512-7fgSH8nVhXSBYPHR/W3tEXXhcnwHwNiND4Dfx9knzPzdsWTUTL/TdDVV+DY0dL0asHKLbdoJaXS4LdVW6R8MVQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -40378,8 +40417,7 @@ "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regexp-clone": { "version": "1.0.0", diff --git a/package.json b/package.json index 45e150f6668..6afd927c17c 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,8 @@ "freeport": "^1.0.5", "gm": "^1.25.0", "html-entities": "^2.3.2", + "i18next": "^23.3.0", + "i18next-fs-backend": "^2.1.5", "jose": "^1.28.1", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^2.0.5", From 3cf0fd5f01fb61932156982be12a316f0345e96d Mon Sep 17 00:00:00 2001 From: Cedric Evers <12080057+CeEv@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:16:06 +0100 Subject: [PATCH 05/23] BC-5655 move shared infra (#4526) --- apps/server/doc/summary.json | 2 +- .../src/apps/helpers/prometheus-metrics.spec.ts | 6 +++--- apps/server/src/apps/helpers/prometheus-metrics.ts | 2 +- apps/server/src/apps/server.app.ts | 2 +- .../database-management.console.api.spec.ts | 2 +- .../src/console/api-test/server-console.api.spec.ts | 2 +- .../src/console/api-test/test-bootstrap.console.ts | 2 +- apps/server/src/console/console.module.ts | 4 ++-- apps/server/src/console/server.console.spec.ts | 2 +- apps/server/src/console/server.console.ts | 2 +- .../src/core/error/filter/global-error.filter.ts | 2 +- .../infra/antivirus/antivirus.module.spec.ts | 0 .../infra/antivirus/antivirus.module.ts | 0 .../infra/antivirus/antivirus.service.spec.ts | 0 .../infra/antivirus/antivirus.service.ts | 0 .../src/{shared => }/infra/antivirus/index.ts | 0 .../infra/antivirus/interfaces/antivirus.ts | 0 .../infra/antivirus/interfaces/index.ts | 2 +- .../src/{shared => }/infra/cache/cache.module.ts | 0 apps/server/src/{shared => }/infra/cache/index.ts | 0 .../infra/cache/interface/cache-store-type.enum.ts | 0 .../src/{shared => }/infra/cache/interface/index.ts | 0 .../infra/cache/service/cache.service.ts | 0 .../{shared => }/infra/calendar/calendar.module.ts | 4 ++-- .../infra/calendar/dto/calendar-event.dto.ts | 0 .../server/src/{shared => }/infra/calendar/index.ts | 0 .../calendar/interface/calendar-event.interface.ts | 0 .../infra/calendar/mapper/calendar.mapper.spec.ts | 4 ++-- .../infra/calendar/mapper/calendar.mapper.ts | 4 ++-- .../infra/calendar/service/calendar.service.spec.ts | 6 +++--- .../infra/calendar/service/calendar.service.ts | 4 ++-- .../collaborative-storage-adapter.module.ts | 6 +++--- .../collaborative-storage.adapter.spec.ts | 8 ++++---- .../collaborative-storage.adapter.ts | 4 ++-- .../dto/team-role-permissions.dto.ts | 0 .../server/src/infra/collaborative-storage/index.ts | 2 ++ .../collaborative-storage-adapter.mapper.spec.ts | 2 +- .../mapper/collaborative-storage-adapter.mapper.ts | 0 .../src/infra/collaborative-storage/mapper/index.ts | 1 + .../strategy/base.interface.strategy.ts | 0 .../nextcloud/httpRequests/NextcloudGroups.http | 0 .../strategy/nextcloud/nextcloud.client.spec.ts | 2 +- .../strategy/nextcloud/nextcloud.client.ts | 2 +- .../strategy/nextcloud/nextcloud.interface.ts | 0 .../strategy/nextcloud/nextcloud.strategy.spec.ts | 6 +++--- .../strategy/nextcloud/nextcloud.strategy.ts | 0 .../strategy/nextcloud/test.json | 0 .../console-writer/console-writer.module.spec.ts | 0 .../console/console-writer/console-writer.module.ts | 0 .../console-writer/console-writer.service.spec.ts | 0 .../console-writer/console-writer.service.ts | 0 .../infra/console/console-writer/index.ts | 0 apps/server/src/{shared => }/infra/console/index.ts | 0 .../server/src/{shared => }/infra/database/index.ts | 0 .../management/database-management.module.spec.ts | 2 +- .../management/database-management.module.ts | 0 .../management/database-management.service.spec.ts | 2 +- .../management/database-management.service.ts | 0 .../{shared => }/infra/database/management/index.ts | 0 .../infra/database/mongo-memory-database/index.ts | 0 .../mongo-memory-database.module.ts | 0 .../infra/database/mongo-memory-database/types.ts | 0 .../infra/encryption/encryption.interface.ts | 0 .../infra/encryption/encryption.module.spec.ts | 0 .../infra/encryption/encryption.module.ts | 0 .../infra/encryption/encryption.service.spec.ts | 0 .../infra/encryption/encryption.service.ts | 0 .../src/{shared => }/infra/encryption/index.ts | 6 +++--- .../feathers/feathers-service.provider.spec.ts | 0 .../infra/feathers/feathers-service.provider.ts | 0 .../{shared => }/infra/feathers/feathers.module.ts | 0 .../server/src/{shared => }/infra/feathers/index.ts | 0 .../infra/file-system/file-system.adapter.spec.ts | 0 .../infra/file-system/file-system.adapter.ts | 0 .../infra/file-system/file-system.module.spec.ts | 0 .../infra/file-system/file-system.module.ts | 0 .../src/{shared => }/infra/file-system/index.ts | 0 .../infra/file-system/utf-8-test-file.txt | 0 .../identity-management-oauth.service.ts | 0 .../identity-management.config.ts | 0 .../identity-management.module.spec.ts | 2 +- .../identity-management.module.ts | 0 .../identity-management.service.ts | 0 .../{shared => }/infra/identity-management/index.ts | 0 .../interface/keycloak-settings.interface.ts | 0 .../keycloak-administration.module.spec.ts | 0 .../keycloak-administration.module.ts | 0 .../keycloak-administration/keycloak-config.ts | 0 .../service/keycloak-administration.service.spec.ts | 0 .../service/keycloak-administration.service.ts | 0 .../console/keycloak-configuration.console.spec.ts | 2 +- .../console/keycloak-configuration.console.ts | 2 +- .../keycloak-configuration.controller.spec.ts | 0 .../controller/keycloak-configuration.controller.ts | 0 .../interface/json-account.interface.ts | 0 .../interface/json-user.interface.ts | 0 .../keycloak-configuration-input-files.interface.ts | 0 .../keycloak-configuration/keycloak-config.ts | 0 .../keycloak-configuration.module.spec.ts | 2 +- .../keycloak-configuration.module.ts | 4 ++-- .../mapper/identity-provider.mapper.spec.ts | 2 +- .../mapper/identity-provider.mapper.ts | 2 +- ...ycloak-configuration.service.integration.spec.ts | 2 +- .../service/keycloak-configuration.service.spec.ts | 2 +- .../service/keycloak-configuration.service.ts | 0 .../keycloak-migration.service.integration.spec.ts | 2 +- .../service/keycloak-migration.service.spec.ts | 0 .../service/keycloak-migration.service.ts | 0 .../keycloak-seed.service.integration.spec.ts | 2 +- .../service/keycloak-seed.service.spec.ts | 0 .../service/keycloak-seed.service.ts | 0 .../uc/keycloak-configuration.spec.ts | 0 .../uc/keycloak-configuration.uc.ts | 0 .../keycloak/keycloak.module.spec.ts | 2 +- .../identity-management/keycloak/keycloak.module.ts | 2 +- ...ity-management-oauth.service.integration.spec.ts | 4 ++-- ...ycloak-identity-management-oauth.service.spec.ts | 2 +- .../keycloak-identity-management-oauth.service.ts | 2 +- ...-identity-management.service.integration.spec.ts | 4 ++-- .../keycloak-identity-management.service.spec.ts | 0 .../service/keycloak-identity-management.service.ts | 0 apps/server/src/{shared => }/infra/index.ts | 0 apps/server/src/{shared => }/infra/mail/index.ts | 0 .../src/{shared => }/infra/mail/mail.interface.ts | 0 .../src/{shared => }/infra/mail/mail.module.spec.ts | 2 +- .../src/{shared => }/infra/mail/mail.module.ts | 0 .../{shared => }/infra/mail/mail.service.spec.ts | 0 .../src/{shared => }/infra/mail/mail.service.ts | 0 apps/server/src/{shared => }/infra/metrics/index.ts | 0 .../infra/metrics/prometheus/app.spec.ts | 0 .../{shared => }/infra/metrics/prometheus/app.ts | 0 .../infra/metrics/prometheus/config.spec.ts | 0 .../{shared => }/infra/metrics/prometheus/config.ts | 0 .../infra/metrics/prometheus/handler.spec.ts | 0 .../infra/metrics/prometheus/handler.ts | 0 .../{shared => }/infra/metrics/prometheus/index.ts | 0 .../infra/metrics/prometheus/middleware.spec.ts | 0 .../infra/metrics/prometheus/middleware.ts | 0 .../{shared => }/infra/oauth-provider/dto/index.ts | 0 .../dto/interface/oauth-client.interface.ts | 0 .../dto/interface/oidc-context.interface.ts | 0 .../dto/request/accept-consent-request.body.ts | 0 .../dto/request/accept-login-request.body.ts | 0 .../dto/request/reject-request.body.ts | 0 .../dto/response/consent-session.response.ts | 0 .../oauth-provider/dto/response/consent.response.ts | 0 .../dto/response/introspect.response.ts | 0 .../oauth-provider/dto/response/login.response.ts | 0 .../dto/response/redirect.response.ts | 0 .../oauth-provider/hydra/hydra.adapter.spec.ts | 6 +++--- .../infra/oauth-provider/hydra/hydra.adapter.ts | 0 .../src/{shared => }/infra/oauth-provider/index.ts | 0 .../oauth-provider/oauth-provider-service.module.ts | 4 ++-- .../infra/oauth-provider/oauth-provider.service.ts | 0 .../{shared => }/infra/preview-generator/index.ts | 8 ++++---- .../infra/preview-generator/interface/index.ts | 2 +- .../interface/preview-consumer-config.ts | 2 +- .../infra/preview-generator/interface/preview.ts | 0 .../loggable/preview-actions.loggable.spec.ts | 0 .../loggable/preview-actions.loggable.ts | 0 .../preview-generator-consumer.module.ts | 4 ++-- .../preview-generator-producer.module.ts | 0 .../preview-generator.builder.spec.ts | 0 .../preview-generator/preview-generator.builder.ts | 2 +- .../preview-generator.consumer.spec.ts | 0 .../preview-generator/preview-generator.consumer.ts | 2 +- .../preview-generator.service.spec.ts | 2 +- .../preview-generator/preview-generator.service.ts | 2 +- .../preview-generator/preview.producer.spec.ts | 0 .../infra/preview-generator/preview.producer.ts | 2 +- .../infra/rabbitmq/error.mapper.spec.ts | 2 +- .../src/{shared => }/infra/rabbitmq/error.mapper.ts | 2 +- .../infra/rabbitmq/exchange/files-preview.ts | 0 .../infra/rabbitmq/exchange/files-storage.ts | 0 .../{shared => }/infra/rabbitmq/exchange/index.ts | 4 ++-- .../server/src/{shared => }/infra/rabbitmq/index.ts | 0 .../{shared => }/infra/rabbitmq/rabbitmq.module.ts | 0 .../infra/rabbitmq/rpc-message-producer.spec.ts | 0 .../infra/rabbitmq/rpc-message-producer.ts | 0 .../src/{shared => }/infra/rabbitmq/rpc-message.ts | 0 apps/server/src/{shared => }/infra/redis/index.ts | 0 .../infra/redis/interface/redis.constants.ts | 0 .../src/{shared => }/infra/redis/redis.module.ts | 0 .../src/{shared => }/infra/s3-client/README.md | 0 .../src/{shared => }/infra/s3-client/constants.ts | 0 .../src/{shared => }/infra/s3-client/index.ts | 6 +++--- .../{shared => }/infra/s3-client/interface/index.ts | 0 .../infra/s3-client/s3-client.adapter.spec.ts | 8 +++----- .../infra/s3-client/s3-client.adapter.ts | 0 .../infra/s3-client/s3-client.module.spec.ts | 0 .../infra/s3-client/s3-client.module.ts | 0 .../src/modules/account/account.module.spec.ts | 2 +- apps/server/src/modules/account/account.module.ts | 2 +- .../account/repo/account.repo.integration.spec.ts | 2 +- .../account/services/account-db.service.spec.ts | 2 +- .../account-idm.service.integration.spec.ts | 5 ++--- .../account/services/account-idm.service.spec.ts | 4 ++-- .../modules/account/services/account-idm.service.ts | 2 +- .../account/services/account-lookup.service.spec.ts | 2 +- .../account/services/account-lookup.service.ts | 2 +- .../services/account.service.integration.spec.ts | 8 ++++---- .../modules/authentication/authentication.module.ts | 4 ++-- apps/server/src/modules/authentication/index.ts | 1 + .../strategy/jwt-validation.adapter.spec.ts | 4 ++-- .../strategy/jwt-validation.adapter.ts | 4 ++-- .../authentication/strategy/local.strategy.spec.ts | 2 +- .../authentication/strategy/local.strategy.ts | 2 +- .../modules/authorization/authorization.module.ts | 2 +- .../feathers/feathers-auth.provider.spec.ts | 2 +- .../feathers/feathers-auth.provider.ts | 2 +- apps/server/src/modules/board/board.module.ts | 2 +- .../src/modules/board/repo/board-do.repo.spec.ts | 2 +- .../src/modules/board/repo/board-node.repo.spec.ts | 2 +- .../board/repo/recursive-delete.visitor.spec.ts | 2 +- .../board-do-copy.service.spec.ts | 2 +- .../board-do-copy-service/recursive-copy.visitor.ts | 2 +- .../modules/board/uc/board-management.uc.spec.ts | 4 ++-- .../src/modules/class/repo/classes.repo.spec.ts | 2 +- .../collaborative-storage.module.ts | 13 ++++++------- .../collaborative-storage/controller/index.ts | 1 + .../src/modules/collaborative-storage/index.ts | 4 ++-- .../modules/collaborative-storage/mapper/index.ts | 2 ++ .../services/collaborative-storage.service.spec.ts | 2 +- .../services/collaborative-storage.service.ts | 2 +- .../src/modules/collaborative-storage/uc/index.ts | 1 + .../modules/deletion/repo/deletion-log.repo.spec.ts | 2 +- .../deletion/repo/deletion-request.repo.spec.ts | 2 +- .../files-storage-client/dto/file.dto.spec.ts | 2 +- .../modules/files-storage-client/dto/file.dto.ts | 2 +- .../interfaces/file-domain-object-props.ts | 2 +- .../interfaces/file-request-info.ts | 2 +- .../copy-files-of-parent-param.builder.spec.ts | 2 +- .../mapper/files-storage-client.mapper.spec.ts | 2 +- .../mapper/files-storage-client.mapper.ts | 2 +- .../mapper/files-storage-param.builder.spec.ts | 2 +- .../service/files-storage.producer.spec.ts | 2 +- .../service/files-storage.producer.ts | 2 +- .../api-test/files-storage-copy-files.api.spec.ts | 4 ++-- .../api-test/files-storage-delete-files.api.spec.ts | 4 ++-- .../files-storage-download-upload.api.spec.ts | 4 ++-- .../api-test/files-storage-preview.api.spec.ts | 6 +++--- .../files-storage-restore-files.api.spec.ts | 4 ++-- .../controller/dto/file-storage.params.ts | 2 +- .../controller/files-storage.consumer.ts | 4 ++-- .../src/modules/files-storage/dto/file.dto.ts | 2 +- .../files-storage/files-preview-amqp.module.ts | 2 +- .../files-storage/files-storage-test.module.ts | 7 +++---- .../modules/files-storage/files-storage.config.ts | 2 +- .../modules/files-storage/files-storage.module.ts | 11 +++++------ .../server/src/modules/files-storage/helper/path.ts | 2 +- .../src/modules/files-storage/helper/test-helper.ts | 2 +- .../files-storage/mapper/file-response.builder.ts | 2 +- .../modules/files-storage/mapper/preview.builder.ts | 2 +- .../repo/filerecord.repo.integration.spec.ts | 2 +- .../service/files-storage-copy.service.spec.ts | 4 ++-- .../service/files-storage-delete.service.spec.ts | 4 ++-- .../service/files-storage-download.service.spec.ts | 4 ++-- .../service/files-storage-get.service.spec.ts | 4 ++-- .../service/files-storage-restore.service.spec.ts | 4 ++-- .../service/files-storage-update.service.spec.ts | 4 ++-- .../service/files-storage-upload.service.spec.ts | 4 ++-- .../files-storage/service/files-storage.service.ts | 4 ++-- .../src/modules/files-storage/service/index.ts | 2 ++ .../files-storage/service/preview.service.spec.ts | 4 ++-- .../files-storage/service/preview.service.ts | 4 ++-- .../files-storage/uc/files-storage-copy.uc.spec.ts | 4 ++-- .../uc/files-storage-delete.uc.spec.ts | 4 ++-- .../uc/files-storage-download-preview.uc.spec.ts | 4 ++-- .../uc/files-storage-download.uc.spec.ts | 4 ++-- .../files-storage/uc/files-storage-get.uc.spec.ts | 4 ++-- .../uc/files-storage-restore.uc.spec.ts | 4 ++-- .../uc/files-storage-update.uc.spec.ts | 4 ++-- .../uc/files-storage-upload.uc.spec.ts | 4 ++-- .../src/modules/files/repo/files.repo.spec.ts | 2 +- .../api-test/fwu-learning-contents.api.spec.ts | 2 +- .../fwu-learning-contents-test.module.ts | 8 ++++---- .../fwu-learning-contents.config.ts | 2 +- .../fwu-learning-contents.module.ts | 4 ++-- .../uc/fwu-learning-contents.uc.spec.ts | 2 +- .../uc/fwu-learning-contents.uc.ts | 2 +- .../src/modules/group/repo/group.repo.spec.ts | 2 +- .../controller/api-test/h5p-editor-ajax.api.spec.ts | 2 +- .../api-test/h5p-editor-delete.api.spec.ts | 2 +- .../api-test/h5p-editor-files.api.spec.ts | 2 +- .../api-test/h5p-editor-get-editor.api.spec.ts | 2 +- .../api-test/h5p-editor-get-player.api.spec.ts | 2 +- .../api-test/h5p-editor-save-create.api.spec.ts | 2 +- .../h5p-editor/controller/dto/h5p-file.dto.ts | 2 +- .../modules/h5p-editor/h5p-editor-test.module.ts | 10 +++++----- .../src/modules/h5p-editor/h5p-editor.config.ts | 2 +- .../src/modules/h5p-editor/h5p-editor.module.ts | 6 +++--- .../repo/h5p-content.repo.integration.spec.ts | 2 +- .../modules/h5p-editor/repo/library.repo.spec.ts | 2 +- .../repo/temporary-file.repo.integration.spec.ts | 2 +- .../service/contentStorage.service.spec.ts | 2 +- .../h5p-editor/service/contentStorage.service.ts | 2 +- .../service/libraryStorage.service.spec.ts | 2 +- .../h5p-editor/service/libraryStorage.service.ts | 2 +- .../service/temporary-file-storage.service.spec.ts | 2 +- .../service/temporary-file-storage.service.ts | 2 +- .../service/column-board-target.service.spec.ts | 2 +- .../repo/schoolyear.repo.integration.spec.ts | 2 +- apps/server/src/modules/lesson/lesson.module.ts | 2 +- .../modules/lesson/service/etherpad.service.spec.ts | 2 +- .../src/modules/lesson/service/etherpad.service.ts | 2 +- .../modules/lesson/service/nexboard.service.spec.ts | 2 +- .../src/modules/lesson/service/nexboard.service.ts | 2 +- .../console/board-management.console.spec.ts | 2 +- .../management/console/board-management.console.ts | 2 +- .../console/database-management.console.spec.ts | 2 +- .../console/database-management.console.ts | 2 +- .../modules/management/management-server.module.ts | 4 ++-- .../src/modules/management/management.module.ts | 10 +++++----- .../modules/management/uc/board-management.uc.ts | 2 +- .../management/uc/database-management.uc.spec.ts | 10 +++------- .../modules/management/uc/database-management.uc.ts | 6 +++--- .../meta-tag-extractor/meta-tag-extractor.module.ts | 2 +- .../controller/oauth-provider.controller.spec.ts | 2 +- .../controller/oauth-provider.controller.ts | 4 ++-- .../mapper/oauth-provider-request.mapper.spec.ts | 2 +- .../mapper/oauth-provider-request.mapper.ts | 2 +- .../mapper/oauth-provider-response.mapper.spec.ts | 2 +- .../mapper/oauth-provider-response.mapper.ts | 2 +- .../oauth-provider/oauth-provider-api.module.ts | 2 +- .../modules/oauth-provider/oauth-provider.module.ts | 2 +- .../uc/oauth-provider.client-crud.uc.spec.ts | 4 ++-- .../uc/oauth-provider.client-crud.uc.ts | 4 ++-- .../uc/oauth-provider.consent-flow.uc.spec.ts | 8 ++------ .../uc/oauth-provider.consent-flow.uc.ts | 4 ++-- .../uc/oauth-provider.login-flow.uc.spec.ts | 4 ++-- .../uc/oauth-provider.login-flow.uc.ts | 8 ++------ .../uc/oauth-provider.logout-flow.uc.spec.ts | 2 +- .../uc/oauth-provider.logout-flow.uc.ts | 4 ++-- .../oauth-provider/uc/oauth-provider.uc.spec.ts | 4 ++-- .../modules/oauth-provider/uc/oauth-provider.uc.ts | 4 ++-- .../oauth/controller/api-test/oauth-sso.api.spec.ts | 2 +- apps/server/src/modules/oauth/oauth.module.ts | 4 ++-- .../src/modules/oauth/service/hydra.service.spec.ts | 2 +- .../src/modules/oauth/service/hydra.service.ts | 2 +- .../src/modules/oauth/service/oauth.service.spec.ts | 2 +- .../src/modules/oauth/service/oauth.service.ts | 2 +- ...external-tool-pseudonym.repo.integration.spec.ts | 2 +- .../modules/pseudonym/repo/pseudonyms.repo.spec.ts | 2 +- apps/server/src/modules/server/server.config.ts | 2 +- apps/server/src/modules/server/server.module.ts | 8 ++++---- .../repo/share-token.repo.integration.spec.ts | 2 +- .../modules/system/service/system.service.spec.ts | 2 +- .../src/modules/system/service/system.service.ts | 2 +- apps/server/src/modules/system/system.module.ts | 2 +- .../modules/task/service/submission.service.spec.ts | 2 +- .../tool/external-tool/external-tool.module.ts | 4 ++-- .../service/external-tool-service.mapper.spec.ts | 2 +- .../service/external-tool-service.mapper.ts | 2 +- .../service/external-tool.service.spec.ts | 6 +++--- .../external-tool/service/external-tool.service.ts | 6 +++--- .../modules/user-import/uc/user-import.uc.spec.ts | 2 +- .../service/video-conference.service.spec.ts | 2 +- .../service/video-conference.service.ts | 2 +- .../uc/video-conference-deprecated.uc.spec.ts | 4 ++-- .../uc/video-conference-deprecated.uc.ts | 4 ++-- .../video-conference/video-conference.module.ts | 2 +- .../src/shared/domain/entity/all-entities.spec.ts | 2 +- .../src/shared/infra/collaborative-storage/index.ts | 1 - .../shared/repo/base.do.repo.integration.spec.ts | 2 +- .../src/shared/repo/base.repo.integration.spec.ts | 2 +- .../server/src/shared/repo/board/board.repo.spec.ts | 2 +- .../context-external-tool.repo.integration.spec.ts | 2 +- .../repo/course/course.repo.integration.spec.ts | 2 +- .../coursegroup.repo.integration.spec.ts | 2 +- .../repo/dashboard/dashboard.model.mapper.spec.ts | 2 +- .../dashboard/dashboard.repo.integration.spec.ts | 2 +- .../external-tool.repo.integration.spec.ts | 2 +- .../repo/federalstate/federal-state.repo.spec.ts | 2 +- .../importuser/importuser.repo.integration.spec.ts | 2 +- .../repo/lesson/lesson.repo.integration.spec.ts | 2 +- .../src/shared/repo/ltitool/ltitool.repo.spec.ts | 2 +- .../materials/materials.repo.integration.spec.ts | 2 +- .../src/shared/repo/news/news-inheritance.spec.ts | 2 +- .../shared/repo/news/news.repo.integration.spec.ts | 2 +- .../shared/repo/role/role.repo.integration.spec.ts | 2 +- .../school/legacy-school.repo.integration.spec.ts | 2 +- .../school-external-tool.repo.integration.spec.ts | 2 +- .../storageprovider/storageprovider.repo.spec.ts | 2 +- .../submission/submission.repo.integration.spec.ts | 2 +- .../repo/system/system.repo.integration.spec.ts | 2 +- .../shared/repo/task/task.repo.integration.spec.ts | 2 +- .../shared/repo/teams/team.repo.integration.spec.ts | 2 +- .../repo/user/user-do.repo.integration.spec.ts | 2 +- .../shared/repo/user/user.repo.integration.spec.ts | 2 +- .../user-login-migration.repo.integration.spec.ts | 2 +- .../videoconference/video-conference.repo.spec.ts | 2 +- .../shared/testing/factory/filerecord.factory.ts | 2 +- jest.config.ts | 1 + tsconfig.json | 1 + 394 files changed, 388 insertions(+), 395 deletions(-) rename apps/server/src/{shared => }/infra/antivirus/antivirus.module.spec.ts (100%) rename apps/server/src/{shared => }/infra/antivirus/antivirus.module.ts (100%) rename apps/server/src/{shared => }/infra/antivirus/antivirus.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/antivirus/antivirus.service.ts (100%) rename apps/server/src/{shared => }/infra/antivirus/index.ts (100%) rename apps/server/src/{shared => }/infra/antivirus/interfaces/antivirus.ts (100%) rename apps/server/src/{shared => }/infra/antivirus/interfaces/index.ts (96%) rename apps/server/src/{shared => }/infra/cache/cache.module.ts (100%) rename apps/server/src/{shared => }/infra/cache/index.ts (100%) rename apps/server/src/{shared => }/infra/cache/interface/cache-store-type.enum.ts (100%) rename apps/server/src/{shared => }/infra/cache/interface/index.ts (100%) rename apps/server/src/{shared => }/infra/cache/service/cache.service.ts (100%) rename apps/server/src/{shared => }/infra/calendar/calendar.module.ts (58%) rename apps/server/src/{shared => }/infra/calendar/dto/calendar-event.dto.ts (100%) rename apps/server/src/{shared => }/infra/calendar/index.ts (100%) rename apps/server/src/{shared => }/infra/calendar/interface/calendar-event.interface.ts (100%) rename apps/server/src/{shared => }/infra/calendar/mapper/calendar.mapper.spec.ts (80%) rename apps/server/src/{shared => }/infra/calendar/mapper/calendar.mapper.ts (62%) rename apps/server/src/{shared => }/infra/calendar/service/calendar.service.spec.ts (90%) rename apps/server/src/{shared => }/infra/calendar/service/calendar.service.ts (91%) rename apps/server/src/{shared => }/infra/collaborative-storage/collaborative-storage-adapter.module.ts (75%) rename apps/server/src/{shared => }/infra/collaborative-storage/collaborative-storage.adapter.spec.ts (89%) rename apps/server/src/{shared => }/infra/collaborative-storage/collaborative-storage.adapter.ts (88%) rename apps/server/src/{shared => }/infra/collaborative-storage/dto/team-role-permissions.dto.ts (100%) create mode 100644 apps/server/src/infra/collaborative-storage/index.ts rename apps/server/src/{shared => }/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts (89%) rename apps/server/src/{shared => }/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts (100%) create mode 100644 apps/server/src/infra/collaborative-storage/mapper/index.ts rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/base.interface.strategy.ts (100%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/httpRequests/NextcloudGroups.http (100%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts (99%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts (99%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/nextcloud.interface.ts (100%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts (98%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts (100%) rename apps/server/src/{shared => }/infra/collaborative-storage/strategy/nextcloud/test.json (100%) rename apps/server/src/{shared => }/infra/console/console-writer/console-writer.module.spec.ts (100%) rename apps/server/src/{shared => }/infra/console/console-writer/console-writer.module.ts (100%) rename apps/server/src/{shared => }/infra/console/console-writer/console-writer.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/console/console-writer/console-writer.service.ts (100%) rename apps/server/src/{shared => }/infra/console/console-writer/index.ts (100%) rename apps/server/src/{shared => }/infra/console/index.ts (100%) rename apps/server/src/{shared => }/infra/database/index.ts (100%) rename apps/server/src/{shared => }/infra/database/management/database-management.module.spec.ts (90%) rename apps/server/src/{shared => }/infra/database/management/database-management.module.ts (100%) rename apps/server/src/{shared => }/infra/database/management/database-management.service.spec.ts (97%) rename apps/server/src/{shared => }/infra/database/management/database-management.service.ts (100%) rename apps/server/src/{shared => }/infra/database/management/index.ts (100%) rename apps/server/src/{shared => }/infra/database/mongo-memory-database/index.ts (100%) rename apps/server/src/{shared => }/infra/database/mongo-memory-database/mongo-memory-database.module.ts (100%) rename apps/server/src/{shared => }/infra/database/mongo-memory-database/types.ts (100%) rename apps/server/src/{shared => }/infra/encryption/encryption.interface.ts (100%) rename apps/server/src/{shared => }/infra/encryption/encryption.module.spec.ts (100%) rename apps/server/src/{shared => }/infra/encryption/encryption.module.ts (100%) rename apps/server/src/{shared => }/infra/encryption/encryption.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/encryption/encryption.service.ts (100%) rename apps/server/src/{shared => }/infra/encryption/index.ts (97%) rename apps/server/src/{shared => }/infra/feathers/feathers-service.provider.spec.ts (100%) rename apps/server/src/{shared => }/infra/feathers/feathers-service.provider.ts (100%) rename apps/server/src/{shared => }/infra/feathers/feathers.module.ts (100%) rename apps/server/src/{shared => }/infra/feathers/index.ts (100%) rename apps/server/src/{shared => }/infra/file-system/file-system.adapter.spec.ts (100%) rename apps/server/src/{shared => }/infra/file-system/file-system.adapter.ts (100%) rename apps/server/src/{shared => }/infra/file-system/file-system.module.spec.ts (100%) rename apps/server/src/{shared => }/infra/file-system/file-system.module.ts (100%) rename apps/server/src/{shared => }/infra/file-system/index.ts (100%) rename apps/server/src/{shared => }/infra/file-system/utf-8-test-file.txt (100%) rename apps/server/src/{shared => }/infra/identity-management/identity-management-oauth.service.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/identity-management.config.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/identity-management.module.spec.ts (92%) rename apps/server/src/{shared => }/infra/identity-management/identity-management.module.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/identity-management.service.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/index.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-administration/keycloak-administration.module.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-administration/keycloak-administration.module.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-administration/keycloak-config.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-administration/service/keycloak-administration.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts (98%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts (98%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/interface/keycloak-configuration-input-files.interface.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/keycloak-config.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts (94%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts (93%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts (98%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts (92%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts (98%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts (99%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts (98%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts (97%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/keycloak.module.spec.ts (94%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/keycloak.module.ts (92%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts (96%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts (99%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts (97%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts (96%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/service/keycloak-identity-management.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/identity-management/keycloak/service/keycloak-identity-management.service.ts (100%) rename apps/server/src/{shared => }/infra/index.ts (100%) rename apps/server/src/{shared => }/infra/mail/index.ts (100%) rename apps/server/src/{shared => }/infra/mail/mail.interface.ts (100%) rename apps/server/src/{shared => }/infra/mail/mail.module.spec.ts (90%) rename apps/server/src/{shared => }/infra/mail/mail.module.ts (100%) rename apps/server/src/{shared => }/infra/mail/mail.service.spec.ts (100%) rename apps/server/src/{shared => }/infra/mail/mail.service.ts (100%) rename apps/server/src/{shared => }/infra/metrics/index.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/app.spec.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/app.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/config.spec.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/config.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/handler.spec.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/handler.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/index.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/middleware.spec.ts (100%) rename apps/server/src/{shared => }/infra/metrics/prometheus/middleware.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/index.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/interface/oauth-client.interface.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/interface/oidc-context.interface.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/request/accept-consent-request.body.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/request/accept-login-request.body.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/request/reject-request.body.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/response/consent-session.response.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/response/consent.response.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/response/introspect.response.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/response/login.response.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/dto/response/redirect.response.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/hydra/hydra.adapter.spec.ts (98%) rename apps/server/src/{shared => }/infra/oauth-provider/hydra/hydra.adapter.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/index.ts (100%) rename apps/server/src/{shared => }/infra/oauth-provider/oauth-provider-service.module.ts (61%) rename apps/server/src/{shared => }/infra/oauth-provider/oauth-provider.service.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/index.ts (97%) rename apps/server/src/{shared => }/infra/preview-generator/interface/index.ts (96%) rename apps/server/src/{shared => }/infra/preview-generator/interface/preview-consumer-config.ts (79%) rename apps/server/src/{shared => }/infra/preview-generator/interface/preview.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/loggable/preview-actions.loggable.spec.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/loggable/preview-actions.loggable.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator-consumer.module.ts (89%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator-producer.module.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator.builder.spec.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator.builder.ts (87%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator.consumer.spec.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator.consumer.ts (92%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator.service.spec.ts (98%) rename apps/server/src/{shared => }/infra/preview-generator/preview-generator.service.ts (96%) rename apps/server/src/{shared => }/infra/preview-generator/preview.producer.spec.ts (100%) rename apps/server/src/{shared => }/infra/preview-generator/preview.producer.ts (97%) rename apps/server/src/{shared => }/infra/rabbitmq/error.mapper.spec.ts (97%) rename apps/server/src/{shared => }/infra/rabbitmq/error.mapper.ts (94%) rename apps/server/src/{shared => }/infra/rabbitmq/exchange/files-preview.ts (100%) rename apps/server/src/{shared => }/infra/rabbitmq/exchange/files-storage.ts (100%) rename apps/server/src/{shared => }/infra/rabbitmq/exchange/index.ts (97%) rename apps/server/src/{shared => }/infra/rabbitmq/index.ts (100%) rename apps/server/src/{shared => }/infra/rabbitmq/rabbitmq.module.ts (100%) rename apps/server/src/{shared => }/infra/rabbitmq/rpc-message-producer.spec.ts (100%) rename apps/server/src/{shared => }/infra/rabbitmq/rpc-message-producer.ts (100%) rename apps/server/src/{shared => }/infra/rabbitmq/rpc-message.ts (100%) rename apps/server/src/{shared => }/infra/redis/index.ts (100%) rename apps/server/src/{shared => }/infra/redis/interface/redis.constants.ts (100%) rename apps/server/src/{shared => }/infra/redis/redis.module.ts (100%) rename apps/server/src/{shared => }/infra/s3-client/README.md (100%) rename apps/server/src/{shared => }/infra/s3-client/constants.ts (100%) rename apps/server/src/{shared => }/infra/s3-client/index.ts (97%) rename apps/server/src/{shared => }/infra/s3-client/interface/index.ts (100%) rename apps/server/src/{shared => }/infra/s3-client/s3-client.adapter.spec.ts (99%) rename apps/server/src/{shared => }/infra/s3-client/s3-client.adapter.ts (100%) rename apps/server/src/{shared => }/infra/s3-client/s3-client.module.spec.ts (100%) rename apps/server/src/{shared => }/infra/s3-client/s3-client.module.ts (100%) create mode 100644 apps/server/src/modules/collaborative-storage/controller/index.ts create mode 100644 apps/server/src/modules/collaborative-storage/mapper/index.ts create mode 100644 apps/server/src/modules/collaborative-storage/uc/index.ts create mode 100644 apps/server/src/modules/files-storage/service/index.ts delete mode 100644 apps/server/src/shared/infra/collaborative-storage/index.ts diff --git a/apps/server/doc/summary.json b/apps/server/doc/summary.json index 834b92cb5f4..2c7489c537a 100644 --- a/apps/server/doc/summary.json +++ b/apps/server/doc/summary.json @@ -61,7 +61,7 @@ }, { "title": "S3ClientModule", - "file": "../src/shared/infra/s3-client/README.md" + "file": "../src/infra/s3-client/README.md" } ] } diff --git a/apps/server/src/apps/helpers/prometheus-metrics.spec.ts b/apps/server/src/apps/helpers/prometheus-metrics.spec.ts index 0c4530f99b8..396fc4865b6 100644 --- a/apps/server/src/apps/helpers/prometheus-metrics.spec.ts +++ b/apps/server/src/apps/helpers/prometheus-metrics.spec.ts @@ -5,7 +5,7 @@ import { PrometheusMetricsConfig, createAPIResponseTimeMetricMiddleware, createPrometheusMetricsApp, -} from '@shared/infra/metrics'; +} from '@infra/metrics'; import { Logger } from '@src/core/logger'; import express, { Express, NextFunction, Request, RequestHandler, Response } from 'express'; import { @@ -15,9 +15,9 @@ import { createAndStartPrometheusMetricsAppIfEnabled, } from './prometheus-metrics'; -jest.mock('@shared/infra/metrics', () => { +jest.mock('@infra/metrics', () => { const moduleMock: unknown = { - ...jest.requireActual('@shared/infra/metrics'), + ...jest.requireActual('@infra/metrics'), createAPIResponseTimeMetricMiddleware: jest.fn(), createPrometheusMetricsApp: jest.fn(), }; diff --git a/apps/server/src/apps/helpers/prometheus-metrics.ts b/apps/server/src/apps/helpers/prometheus-metrics.ts index 751cada4c2f..56d04b85d89 100644 --- a/apps/server/src/apps/helpers/prometheus-metrics.ts +++ b/apps/server/src/apps/helpers/prometheus-metrics.ts @@ -4,7 +4,7 @@ import { PrometheusMetricsConfig, createAPIResponseTimeMetricMiddleware, createPrometheusMetricsApp, -} from '@shared/infra/metrics'; +} from '@infra/metrics'; import { LogMessage, Loggable, Logger } from '@src/core/logger'; import { AppStartLoggable } from './app-start-loggable'; diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index bd235d5261f..9538d6c8515 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -5,7 +5,7 @@ import { MikroORM } from '@mikro-orm/core'; import { NestFactory } from '@nestjs/core'; import { ExpressAdapter } from '@nestjs/platform-express'; import { enableOpenApiDocs } from '@shared/controller/swagger'; -import { Mail, MailService } from '@shared/infra/mail'; +import { Mail, MailService } from '@infra/mail'; import { LegacyLogger, Logger } from '@src/core/logger'; import { AccountService } from '@modules/account/services/account.service'; import { TeamService } from '@modules/teams/service/team.service'; diff --git a/apps/server/src/console/api-test/database-management.console.api.spec.ts b/apps/server/src/console/api-test/database-management.console.api.spec.ts index 6a2a8de7e7f..ea3cc340616 100644 --- a/apps/server/src/console/api-test/database-management.console.api.spec.ts +++ b/apps/server/src/console/api-test/database-management.console.api.spec.ts @@ -1,5 +1,5 @@ import { INestApplicationContext } from '@nestjs/common'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { ServerConsoleModule } from '@src/console/console.module'; import { CommanderError } from 'commander'; import { BootstrapConsole, ConsoleService } from 'nestjs-console'; diff --git a/apps/server/src/console/api-test/server-console.api.spec.ts b/apps/server/src/console/api-test/server-console.api.spec.ts index 0621a809c36..d9b71bd2fbc 100644 --- a/apps/server/src/console/api-test/server-console.api.spec.ts +++ b/apps/server/src/console/api-test/server-console.api.spec.ts @@ -2,7 +2,7 @@ import { INestApplicationContext } from '@nestjs/common'; import { BootstrapConsole, ConsoleService } from 'nestjs-console'; import { ServerConsoleModule } from '@src/console/console.module'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { execute, TestBootstrapConsole } from './test-bootstrap.console'; describe('ServerConsole (API)', () => { diff --git a/apps/server/src/console/api-test/test-bootstrap.console.ts b/apps/server/src/console/api-test/test-bootstrap.console.ts index edb196b6a54..f346720bd22 100644 --- a/apps/server/src/console/api-test/test-bootstrap.console.ts +++ b/apps/server/src/console/api-test/test-bootstrap.console.ts @@ -1,6 +1,6 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { DatabaseManagementUc } from '@modules/management/uc/database-management.uc'; import { AbstractBootstrapConsole, BootstrapConsole } from 'nestjs-console'; diff --git a/apps/server/src/console/console.module.ts b/apps/server/src/console/console.module.ts index 2cad08943eb..9d30db8fedb 100644 --- a/apps/server/src/console/console.module.ts +++ b/apps/server/src/console/console.module.ts @@ -4,8 +4,8 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain'; -import { ConsoleWriterModule } from '@shared/infra/console/console-writer/console-writer.module'; -import { KeycloakModule } from '@shared/infra/identity-management/keycloak/keycloak.module'; +import { ConsoleWriterModule } from '@infra/console/console-writer/console-writer.module'; +import { KeycloakModule } from '@infra/identity-management/keycloak/keycloak.module'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { FilesModule } from '@modules/files'; import { FileEntity } from '@modules/files/entity'; diff --git a/apps/server/src/console/server.console.spec.ts b/apps/server/src/console/server.console.spec.ts index 60efe0c962f..46ba3fff065 100644 --- a/apps/server/src/console/server.console.spec.ts +++ b/apps/server/src/console/server.console.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { ServerConsoleModule } from './console.module'; import { ServerConsole } from './server.console'; diff --git a/apps/server/src/console/server.console.ts b/apps/server/src/console/server.console.ts index 0b0d0b45c4a..32ff8182241 100644 --- a/apps/server/src/console/server.console.ts +++ b/apps/server/src/console/server.console.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { Command, Console } from 'nestjs-console'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; @Console({ command: 'server', description: 'sample server console' }) export class ServerConsole { diff --git a/apps/server/src/core/error/filter/global-error.filter.ts b/apps/server/src/core/error/filter/global-error.filter.ts index 56760b18dd9..314c8247d18 100644 --- a/apps/server/src/core/error/filter/global-error.filter.ts +++ b/apps/server/src/core/error/filter/global-error.filter.ts @@ -1,6 +1,6 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException, InternalServerErrorException } from '@nestjs/common'; import { ApiValidationError, BusinessError } from '@shared/common'; -import { IError, RpcMessage } from '@shared/infra/rabbitmq/rpc-message'; +import { IError, RpcMessage } from '@infra/rabbitmq/rpc-message'; import { ErrorLogger, Loggable } from '@src/core/logger'; import { LoggingUtils } from '@src/core/logger/logging.utils'; import { Response } from 'express'; diff --git a/apps/server/src/shared/infra/antivirus/antivirus.module.spec.ts b/apps/server/src/infra/antivirus/antivirus.module.spec.ts similarity index 100% rename from apps/server/src/shared/infra/antivirus/antivirus.module.spec.ts rename to apps/server/src/infra/antivirus/antivirus.module.spec.ts diff --git a/apps/server/src/shared/infra/antivirus/antivirus.module.ts b/apps/server/src/infra/antivirus/antivirus.module.ts similarity index 100% rename from apps/server/src/shared/infra/antivirus/antivirus.module.ts rename to apps/server/src/infra/antivirus/antivirus.module.ts diff --git a/apps/server/src/shared/infra/antivirus/antivirus.service.spec.ts b/apps/server/src/infra/antivirus/antivirus.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/antivirus/antivirus.service.spec.ts rename to apps/server/src/infra/antivirus/antivirus.service.spec.ts diff --git a/apps/server/src/shared/infra/antivirus/antivirus.service.ts b/apps/server/src/infra/antivirus/antivirus.service.ts similarity index 100% rename from apps/server/src/shared/infra/antivirus/antivirus.service.ts rename to apps/server/src/infra/antivirus/antivirus.service.ts diff --git a/apps/server/src/shared/infra/antivirus/index.ts b/apps/server/src/infra/antivirus/index.ts similarity index 100% rename from apps/server/src/shared/infra/antivirus/index.ts rename to apps/server/src/infra/antivirus/index.ts diff --git a/apps/server/src/shared/infra/antivirus/interfaces/antivirus.ts b/apps/server/src/infra/antivirus/interfaces/antivirus.ts similarity index 100% rename from apps/server/src/shared/infra/antivirus/interfaces/antivirus.ts rename to apps/server/src/infra/antivirus/interfaces/antivirus.ts diff --git a/apps/server/src/shared/infra/antivirus/interfaces/index.ts b/apps/server/src/infra/antivirus/interfaces/index.ts similarity index 96% rename from apps/server/src/shared/infra/antivirus/interfaces/index.ts rename to apps/server/src/infra/antivirus/interfaces/index.ts index 6c4771f9cd5..b41764286b4 100644 --- a/apps/server/src/shared/infra/antivirus/interfaces/index.ts +++ b/apps/server/src/infra/antivirus/interfaces/index.ts @@ -1 +1 @@ -export * from './antivirus'; +export * from './antivirus'; diff --git a/apps/server/src/shared/infra/cache/cache.module.ts b/apps/server/src/infra/cache/cache.module.ts similarity index 100% rename from apps/server/src/shared/infra/cache/cache.module.ts rename to apps/server/src/infra/cache/cache.module.ts diff --git a/apps/server/src/shared/infra/cache/index.ts b/apps/server/src/infra/cache/index.ts similarity index 100% rename from apps/server/src/shared/infra/cache/index.ts rename to apps/server/src/infra/cache/index.ts diff --git a/apps/server/src/shared/infra/cache/interface/cache-store-type.enum.ts b/apps/server/src/infra/cache/interface/cache-store-type.enum.ts similarity index 100% rename from apps/server/src/shared/infra/cache/interface/cache-store-type.enum.ts rename to apps/server/src/infra/cache/interface/cache-store-type.enum.ts diff --git a/apps/server/src/shared/infra/cache/interface/index.ts b/apps/server/src/infra/cache/interface/index.ts similarity index 100% rename from apps/server/src/shared/infra/cache/interface/index.ts rename to apps/server/src/infra/cache/interface/index.ts diff --git a/apps/server/src/shared/infra/cache/service/cache.service.ts b/apps/server/src/infra/cache/service/cache.service.ts similarity index 100% rename from apps/server/src/shared/infra/cache/service/cache.service.ts rename to apps/server/src/infra/cache/service/cache.service.ts diff --git a/apps/server/src/shared/infra/calendar/calendar.module.ts b/apps/server/src/infra/calendar/calendar.module.ts similarity index 58% rename from apps/server/src/shared/infra/calendar/calendar.module.ts rename to apps/server/src/infra/calendar/calendar.module.ts index feb0611fcdc..b4eddacef92 100644 --- a/apps/server/src/shared/infra/calendar/calendar.module.ts +++ b/apps/server/src/infra/calendar/calendar.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; -import { CalendarService } from '@shared/infra/calendar/service/calendar.service'; -import { CalendarMapper } from '@shared/infra/calendar/mapper/calendar.mapper'; +import { CalendarService } from './service/calendar.service'; +import { CalendarMapper } from './mapper/calendar.mapper'; @Module({ imports: [HttpModule], diff --git a/apps/server/src/shared/infra/calendar/dto/calendar-event.dto.ts b/apps/server/src/infra/calendar/dto/calendar-event.dto.ts similarity index 100% rename from apps/server/src/shared/infra/calendar/dto/calendar-event.dto.ts rename to apps/server/src/infra/calendar/dto/calendar-event.dto.ts diff --git a/apps/server/src/shared/infra/calendar/index.ts b/apps/server/src/infra/calendar/index.ts similarity index 100% rename from apps/server/src/shared/infra/calendar/index.ts rename to apps/server/src/infra/calendar/index.ts diff --git a/apps/server/src/shared/infra/calendar/interface/calendar-event.interface.ts b/apps/server/src/infra/calendar/interface/calendar-event.interface.ts similarity index 100% rename from apps/server/src/shared/infra/calendar/interface/calendar-event.interface.ts rename to apps/server/src/infra/calendar/interface/calendar-event.interface.ts diff --git a/apps/server/src/shared/infra/calendar/mapper/calendar.mapper.spec.ts b/apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts similarity index 80% rename from apps/server/src/shared/infra/calendar/mapper/calendar.mapper.spec.ts rename to apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts index 512173c8ccb..9679ce0a4fa 100644 --- a/apps/server/src/shared/infra/calendar/mapper/calendar.mapper.spec.ts +++ b/apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts @@ -1,6 +1,6 @@ -import { ICalendarEvent } from '@shared/infra/calendar/interface/calendar-event.interface'; +import { ICalendarEvent } from '@infra/calendar/interface/calendar-event.interface'; import { Test, TestingModule } from '@nestjs/testing'; -import { CalendarMapper } from '@shared/infra/calendar/mapper/calendar.mapper'; +import { CalendarMapper } from './calendar.mapper'; describe('CalendarMapper', () => { let module: TestingModule; diff --git a/apps/server/src/shared/infra/calendar/mapper/calendar.mapper.ts b/apps/server/src/infra/calendar/mapper/calendar.mapper.ts similarity index 62% rename from apps/server/src/shared/infra/calendar/mapper/calendar.mapper.ts rename to apps/server/src/infra/calendar/mapper/calendar.mapper.ts index 8a71dbcb75f..a75ff01eae6 100644 --- a/apps/server/src/shared/infra/calendar/mapper/calendar.mapper.ts +++ b/apps/server/src/infra/calendar/mapper/calendar.mapper.ts @@ -1,6 +1,6 @@ -import { ICalendarEvent } from '@shared/infra/calendar/interface/calendar-event.interface'; +import { ICalendarEvent } from '@infra/calendar/interface/calendar-event.interface'; import { Injectable } from '@nestjs/common'; -import { CalendarEventDto } from '@shared/infra/calendar/dto/calendar-event.dto'; +import { CalendarEventDto } from '../dto/calendar-event.dto'; @Injectable() export class CalendarMapper { diff --git a/apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts b/apps/server/src/infra/calendar/service/calendar.service.spec.ts similarity index 90% rename from apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts rename to apps/server/src/infra/calendar/service/calendar.service.spec.ts index 43ca5ad06d4..ed6bb4620bc 100644 --- a/apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts +++ b/apps/server/src/infra/calendar/service/calendar.service.spec.ts @@ -3,12 +3,12 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { CalendarEventDto, CalendarService } from '@shared/infra/calendar'; -import { ICalendarEvent } from '@shared/infra/calendar/interface/calendar-event.interface'; -import { CalendarMapper } from '@shared/infra/calendar/mapper/calendar.mapper'; +import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { axiosResponseFactory } from '@shared/testing'; import { AxiosResponse } from 'axios'; import { of, throwError } from 'rxjs'; +import { CalendarMapper } from '../mapper/calendar.mapper'; +import { ICalendarEvent } from '../interface/calendar-event.interface'; describe('CalendarServiceSpec', () => { let module: TestingModule; diff --git a/apps/server/src/shared/infra/calendar/service/calendar.service.ts b/apps/server/src/infra/calendar/service/calendar.service.ts similarity index 91% rename from apps/server/src/shared/infra/calendar/service/calendar.service.ts rename to apps/server/src/infra/calendar/service/calendar.service.ts index b79564634a5..3bf2a6576be 100644 --- a/apps/server/src/shared/infra/calendar/service/calendar.service.ts +++ b/apps/server/src/infra/calendar/service/calendar.service.ts @@ -2,12 +2,12 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { CalendarEventDto } from '@shared/infra/calendar/dto/calendar-event.dto'; -import { CalendarMapper } from '@shared/infra/calendar/mapper/calendar.mapper'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Observable, firstValueFrom } from 'rxjs'; import { URL, URLSearchParams } from 'url'; +import { CalendarMapper } from '../mapper/calendar.mapper'; +import { CalendarEventDto } from '../dto/calendar-event.dto'; import { ICalendarEvent } from '../interface/calendar-event.interface'; @Injectable() diff --git a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts b/apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts similarity index 75% rename from apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts rename to apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts index 84e4f4596d6..f60ff664654 100644 --- a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts +++ b/apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts @@ -1,14 +1,14 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpModule } from '@nestjs/axios'; import { Module, Provider } from '@nestjs/common'; -import { CollaborativeStorageAdapterMapper } from '@shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; -import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client'; -import { NextcloudStrategy } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy'; import { LtiToolRepo } from '@shared/repo/ltitool/'; import { LoggerModule } from '@src/core/logger'; import { ToolModule } from '@modules/tool'; import { PseudonymModule } from '@modules/pseudonym'; import { UserModule } from '@modules/user'; +import { NextcloudStrategy } from './strategy/nextcloud/nextcloud.strategy'; +import { NextcloudClient } from './strategy/nextcloud/nextcloud.client'; +import { CollaborativeStorageAdapterMapper } from './mapper'; import { CollaborativeStorageAdapter } from './collaborative-storage.adapter'; const storageStrategy: Provider = { diff --git a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts similarity index 89% rename from apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts rename to apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts index c33f9be282a..3b9ba2175e6 100644 --- a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts +++ b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts @@ -2,11 +2,11 @@ import { createMock } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; -import { CollaborativeStorageAdapter } from '@shared/infra/collaborative-storage/collaborative-storage.adapter'; -import { CollaborativeStorageAdapterMapper } from '@shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; -import { ICollaborativeStorageStrategy } from '@shared/infra/collaborative-storage/strategy/base.interface.strategy'; import { LegacyLogger } from '@src/core/logger'; -import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; // invalid import please fix +import { CollaborativeStorageAdapter } from './collaborative-storage.adapter'; +import { CollaborativeStorageAdapterMapper } from './mapper/collaborative-storage-adapter.mapper'; +import { ICollaborativeStorageStrategy } from './strategy/base.interface.strategy'; class TestStrategy implements ICollaborativeStorageStrategy { baseURL: string; diff --git a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts similarity index 88% rename from apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts rename to apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts index 9edcafbdc12..b50657f393e 100644 --- a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts +++ b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts @@ -1,10 +1,10 @@ import { TeamPermissionsDto } from '@modules/collaborative-storage/services/dto/team-permissions.dto'; import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; -import { ICollaborativeStorageStrategy } from '@shared/infra/collaborative-storage/strategy/base.interface.strategy'; import { Inject, Injectable } from '@nestjs/common'; -import { CollaborativeStorageAdapterMapper } from '@shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; import { LegacyLogger } from '@src/core/logger'; import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { CollaborativeStorageAdapterMapper } from './mapper/collaborative-storage-adapter.mapper'; +import { ICollaborativeStorageStrategy } from './strategy/base.interface.strategy'; /** * Provides an Adapter to an external collaborative storage. diff --git a/apps/server/src/shared/infra/collaborative-storage/dto/team-role-permissions.dto.ts b/apps/server/src/infra/collaborative-storage/dto/team-role-permissions.dto.ts similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/dto/team-role-permissions.dto.ts rename to apps/server/src/infra/collaborative-storage/dto/team-role-permissions.dto.ts diff --git a/apps/server/src/infra/collaborative-storage/index.ts b/apps/server/src/infra/collaborative-storage/index.ts new file mode 100644 index 00000000000..49dea4522b3 --- /dev/null +++ b/apps/server/src/infra/collaborative-storage/index.ts @@ -0,0 +1,2 @@ +export { CollaborativeStorageAdapter } from './collaborative-storage.adapter'; +export { CollaborativeStorageAdapterModule } from './collaborative-storage-adapter.module'; diff --git a/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts b/apps/server/src/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts similarity index 89% rename from apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts rename to apps/server/src/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts index cba6ef365c2..bea98c50ebe 100644 --- a/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts +++ b/apps/server/src/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; -import { CollaborativeStorageAdapterMapper } from '@shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; +import { CollaborativeStorageAdapterMapper } from '@infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; describe('TeamStorage Mapper', () => { let module: TestingModule; diff --git a/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts b/apps/server/src/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts rename to apps/server/src/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts diff --git a/apps/server/src/infra/collaborative-storage/mapper/index.ts b/apps/server/src/infra/collaborative-storage/mapper/index.ts new file mode 100644 index 00000000000..08f05350be2 --- /dev/null +++ b/apps/server/src/infra/collaborative-storage/mapper/index.ts @@ -0,0 +1 @@ +export * from './collaborative-storage-adapter.mapper'; diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/base.interface.strategy.ts b/apps/server/src/infra/collaborative-storage/strategy/base.interface.strategy.ts similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/strategy/base.interface.strategy.ts rename to apps/server/src/infra/collaborative-storage/strategy/base.interface.strategy.ts diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/httpRequests/NextcloudGroups.http b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/httpRequests/NextcloudGroups.http similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/httpRequests/NextcloudGroups.http rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/httpRequests/NextcloudGroups.http diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts similarity index 99% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts index 5aa4cb8b09d..225ddac258d 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts +++ b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts @@ -3,11 +3,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { NotFoundException, NotImplementedException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import { Observable, of } from 'rxjs'; +import { NextcloudClient } from './nextcloud.client'; import { GroupUsers, GroupfoldersCreated, diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts similarity index 99% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts index 75caf1bc0e4..ab4a139224c 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts +++ b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.ts @@ -15,7 +15,7 @@ import { NextcloudGroups, OcsResponse, SuccessfulRes, -} from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.interface'; +} from '@infra/collaborative-storage/strategy/nextcloud/nextcloud.interface'; import { ErrorUtils } from '@src/core/error/utils'; import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.interface.ts b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.interface.ts similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.interface.ts rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.interface.ts diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts similarity index 98% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts index 7684b14dbb0..706e360b700 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts +++ b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts @@ -3,9 +3,6 @@ import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiPrivacyPermission, LtiRoleType, Pseudonym, RoleName, User, UserDO } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { TeamRolePermissionsDto } from '@shared/infra/collaborative-storage/dto/team-role-permissions.dto'; -import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client'; -import { NextcloudStrategy } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy'; import { LtiToolRepo } from '@shared/repo'; import { ltiToolDOFactory, pseudonymFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; @@ -13,6 +10,9 @@ import { TeamDto, TeamUserDto } from '@modules/collaborative-storage/services/dt import { PseudonymService } from '@modules/pseudonym'; import { ExternalToolService } from '@modules/tool/external-tool/service'; import { UserService } from '@modules/user'; +import { NextcloudStrategy } from './nextcloud.strategy'; +import { NextcloudClient } from './nextcloud.client'; +import { TeamRolePermissionsDto } from '../../dto/team-role-permissions.dto'; class NextcloudStrategySpec extends NextcloudStrategy { static specGenerateGroupId(dto: TeamRolePermissionsDto): string { diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/test.json b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/test.json similarity index 100% rename from apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/test.json rename to apps/server/src/infra/collaborative-storage/strategy/nextcloud/test.json diff --git a/apps/server/src/shared/infra/console/console-writer/console-writer.module.spec.ts b/apps/server/src/infra/console/console-writer/console-writer.module.spec.ts similarity index 100% rename from apps/server/src/shared/infra/console/console-writer/console-writer.module.spec.ts rename to apps/server/src/infra/console/console-writer/console-writer.module.spec.ts diff --git a/apps/server/src/shared/infra/console/console-writer/console-writer.module.ts b/apps/server/src/infra/console/console-writer/console-writer.module.ts similarity index 100% rename from apps/server/src/shared/infra/console/console-writer/console-writer.module.ts rename to apps/server/src/infra/console/console-writer/console-writer.module.ts diff --git a/apps/server/src/shared/infra/console/console-writer/console-writer.service.spec.ts b/apps/server/src/infra/console/console-writer/console-writer.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/console/console-writer/console-writer.service.spec.ts rename to apps/server/src/infra/console/console-writer/console-writer.service.spec.ts diff --git a/apps/server/src/shared/infra/console/console-writer/console-writer.service.ts b/apps/server/src/infra/console/console-writer/console-writer.service.ts similarity index 100% rename from apps/server/src/shared/infra/console/console-writer/console-writer.service.ts rename to apps/server/src/infra/console/console-writer/console-writer.service.ts diff --git a/apps/server/src/shared/infra/console/console-writer/index.ts b/apps/server/src/infra/console/console-writer/index.ts similarity index 100% rename from apps/server/src/shared/infra/console/console-writer/index.ts rename to apps/server/src/infra/console/console-writer/index.ts diff --git a/apps/server/src/shared/infra/console/index.ts b/apps/server/src/infra/console/index.ts similarity index 100% rename from apps/server/src/shared/infra/console/index.ts rename to apps/server/src/infra/console/index.ts diff --git a/apps/server/src/shared/infra/database/index.ts b/apps/server/src/infra/database/index.ts similarity index 100% rename from apps/server/src/shared/infra/database/index.ts rename to apps/server/src/infra/database/index.ts diff --git a/apps/server/src/shared/infra/database/management/database-management.module.spec.ts b/apps/server/src/infra/database/management/database-management.module.spec.ts similarity index 90% rename from apps/server/src/shared/infra/database/management/database-management.module.spec.ts rename to apps/server/src/infra/database/management/database-management.module.spec.ts index 2ea05d7a121..1f80c58b5d4 100644 --- a/apps/server/src/shared/infra/database/management/database-management.module.spec.ts +++ b/apps/server/src/infra/database/management/database-management.module.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { DatabaseManagementModule } from './database-management.module'; import { DatabaseManagementService } from './database-management.service'; diff --git a/apps/server/src/shared/infra/database/management/database-management.module.ts b/apps/server/src/infra/database/management/database-management.module.ts similarity index 100% rename from apps/server/src/shared/infra/database/management/database-management.module.ts rename to apps/server/src/infra/database/management/database-management.module.ts diff --git a/apps/server/src/shared/infra/database/management/database-management.service.spec.ts b/apps/server/src/infra/database/management/database-management.service.spec.ts similarity index 97% rename from apps/server/src/shared/infra/database/management/database-management.service.spec.ts rename to apps/server/src/infra/database/management/database-management.service.spec.ts index 2487fda1280..7bbe7bdc5e3 100644 --- a/apps/server/src/shared/infra/database/management/database-management.service.spec.ts +++ b/apps/server/src/infra/database/management/database-management.service.spec.ts @@ -1,6 +1,6 @@ import { MikroORM } from '@mikro-orm/core'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ObjectId } from 'mongodb'; import { DatabaseManagementService } from './database-management.service'; diff --git a/apps/server/src/shared/infra/database/management/database-management.service.ts b/apps/server/src/infra/database/management/database-management.service.ts similarity index 100% rename from apps/server/src/shared/infra/database/management/database-management.service.ts rename to apps/server/src/infra/database/management/database-management.service.ts diff --git a/apps/server/src/shared/infra/database/management/index.ts b/apps/server/src/infra/database/management/index.ts similarity index 100% rename from apps/server/src/shared/infra/database/management/index.ts rename to apps/server/src/infra/database/management/index.ts diff --git a/apps/server/src/shared/infra/database/mongo-memory-database/index.ts b/apps/server/src/infra/database/mongo-memory-database/index.ts similarity index 100% rename from apps/server/src/shared/infra/database/mongo-memory-database/index.ts rename to apps/server/src/infra/database/mongo-memory-database/index.ts diff --git a/apps/server/src/shared/infra/database/mongo-memory-database/mongo-memory-database.module.ts b/apps/server/src/infra/database/mongo-memory-database/mongo-memory-database.module.ts similarity index 100% rename from apps/server/src/shared/infra/database/mongo-memory-database/mongo-memory-database.module.ts rename to apps/server/src/infra/database/mongo-memory-database/mongo-memory-database.module.ts diff --git a/apps/server/src/shared/infra/database/mongo-memory-database/types.ts b/apps/server/src/infra/database/mongo-memory-database/types.ts similarity index 100% rename from apps/server/src/shared/infra/database/mongo-memory-database/types.ts rename to apps/server/src/infra/database/mongo-memory-database/types.ts diff --git a/apps/server/src/shared/infra/encryption/encryption.interface.ts b/apps/server/src/infra/encryption/encryption.interface.ts similarity index 100% rename from apps/server/src/shared/infra/encryption/encryption.interface.ts rename to apps/server/src/infra/encryption/encryption.interface.ts diff --git a/apps/server/src/shared/infra/encryption/encryption.module.spec.ts b/apps/server/src/infra/encryption/encryption.module.spec.ts similarity index 100% rename from apps/server/src/shared/infra/encryption/encryption.module.spec.ts rename to apps/server/src/infra/encryption/encryption.module.spec.ts diff --git a/apps/server/src/shared/infra/encryption/encryption.module.ts b/apps/server/src/infra/encryption/encryption.module.ts similarity index 100% rename from apps/server/src/shared/infra/encryption/encryption.module.ts rename to apps/server/src/infra/encryption/encryption.module.ts diff --git a/apps/server/src/shared/infra/encryption/encryption.service.spec.ts b/apps/server/src/infra/encryption/encryption.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/encryption/encryption.service.spec.ts rename to apps/server/src/infra/encryption/encryption.service.spec.ts diff --git a/apps/server/src/shared/infra/encryption/encryption.service.ts b/apps/server/src/infra/encryption/encryption.service.ts similarity index 100% rename from apps/server/src/shared/infra/encryption/encryption.service.ts rename to apps/server/src/infra/encryption/encryption.service.ts diff --git a/apps/server/src/shared/infra/encryption/index.ts b/apps/server/src/infra/encryption/index.ts similarity index 97% rename from apps/server/src/shared/infra/encryption/index.ts rename to apps/server/src/infra/encryption/index.ts index 201f12060e4..39019741bd0 100644 --- a/apps/server/src/shared/infra/encryption/index.ts +++ b/apps/server/src/infra/encryption/index.ts @@ -1,3 +1,3 @@ -export * from './encryption.module'; -export * from './encryption.interface'; -export * from './encryption.service'; +export * from './encryption.module'; +export * from './encryption.interface'; +export * from './encryption.service'; diff --git a/apps/server/src/shared/infra/feathers/feathers-service.provider.spec.ts b/apps/server/src/infra/feathers/feathers-service.provider.spec.ts similarity index 100% rename from apps/server/src/shared/infra/feathers/feathers-service.provider.spec.ts rename to apps/server/src/infra/feathers/feathers-service.provider.spec.ts diff --git a/apps/server/src/shared/infra/feathers/feathers-service.provider.ts b/apps/server/src/infra/feathers/feathers-service.provider.ts similarity index 100% rename from apps/server/src/shared/infra/feathers/feathers-service.provider.ts rename to apps/server/src/infra/feathers/feathers-service.provider.ts diff --git a/apps/server/src/shared/infra/feathers/feathers.module.ts b/apps/server/src/infra/feathers/feathers.module.ts similarity index 100% rename from apps/server/src/shared/infra/feathers/feathers.module.ts rename to apps/server/src/infra/feathers/feathers.module.ts diff --git a/apps/server/src/shared/infra/feathers/index.ts b/apps/server/src/infra/feathers/index.ts similarity index 100% rename from apps/server/src/shared/infra/feathers/index.ts rename to apps/server/src/infra/feathers/index.ts diff --git a/apps/server/src/shared/infra/file-system/file-system.adapter.spec.ts b/apps/server/src/infra/file-system/file-system.adapter.spec.ts similarity index 100% rename from apps/server/src/shared/infra/file-system/file-system.adapter.spec.ts rename to apps/server/src/infra/file-system/file-system.adapter.spec.ts diff --git a/apps/server/src/shared/infra/file-system/file-system.adapter.ts b/apps/server/src/infra/file-system/file-system.adapter.ts similarity index 100% rename from apps/server/src/shared/infra/file-system/file-system.adapter.ts rename to apps/server/src/infra/file-system/file-system.adapter.ts diff --git a/apps/server/src/shared/infra/file-system/file-system.module.spec.ts b/apps/server/src/infra/file-system/file-system.module.spec.ts similarity index 100% rename from apps/server/src/shared/infra/file-system/file-system.module.spec.ts rename to apps/server/src/infra/file-system/file-system.module.spec.ts diff --git a/apps/server/src/shared/infra/file-system/file-system.module.ts b/apps/server/src/infra/file-system/file-system.module.ts similarity index 100% rename from apps/server/src/shared/infra/file-system/file-system.module.ts rename to apps/server/src/infra/file-system/file-system.module.ts diff --git a/apps/server/src/shared/infra/file-system/index.ts b/apps/server/src/infra/file-system/index.ts similarity index 100% rename from apps/server/src/shared/infra/file-system/index.ts rename to apps/server/src/infra/file-system/index.ts diff --git a/apps/server/src/shared/infra/file-system/utf-8-test-file.txt b/apps/server/src/infra/file-system/utf-8-test-file.txt similarity index 100% rename from apps/server/src/shared/infra/file-system/utf-8-test-file.txt rename to apps/server/src/infra/file-system/utf-8-test-file.txt diff --git a/apps/server/src/shared/infra/identity-management/identity-management-oauth.service.ts b/apps/server/src/infra/identity-management/identity-management-oauth.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/identity-management-oauth.service.ts rename to apps/server/src/infra/identity-management/identity-management-oauth.service.ts diff --git a/apps/server/src/shared/infra/identity-management/identity-management.config.ts b/apps/server/src/infra/identity-management/identity-management.config.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/identity-management.config.ts rename to apps/server/src/infra/identity-management/identity-management.config.ts diff --git a/apps/server/src/shared/infra/identity-management/identity-management.module.spec.ts b/apps/server/src/infra/identity-management/identity-management.module.spec.ts similarity index 92% rename from apps/server/src/shared/infra/identity-management/identity-management.module.spec.ts rename to apps/server/src/infra/identity-management/identity-management.module.spec.ts index 9bbfe6d1f93..e81186d7562 100644 --- a/apps/server/src/shared/infra/identity-management/identity-management.module.spec.ts +++ b/apps/server/src/infra/identity-management/identity-management.module.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ConfigModule } from '@nestjs/config'; import { IdentityManagementService } from './identity-management.service'; import { IdentityManagementModule } from './identity-management.module'; diff --git a/apps/server/src/shared/infra/identity-management/identity-management.module.ts b/apps/server/src/infra/identity-management/identity-management.module.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/identity-management.module.ts rename to apps/server/src/infra/identity-management/identity-management.module.ts diff --git a/apps/server/src/shared/infra/identity-management/identity-management.service.ts b/apps/server/src/infra/identity-management/identity-management.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/identity-management.service.ts rename to apps/server/src/infra/identity-management/identity-management.service.ts diff --git a/apps/server/src/shared/infra/identity-management/index.ts b/apps/server/src/infra/identity-management/index.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/index.ts rename to apps/server/src/infra/identity-management/index.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts b/apps/server/src/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts rename to apps/server/src/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-administration/keycloak-administration.module.spec.ts b/apps/server/src/infra/identity-management/keycloak-administration/keycloak-administration.module.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-administration/keycloak-administration.module.spec.ts rename to apps/server/src/infra/identity-management/keycloak-administration/keycloak-administration.module.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-administration/keycloak-administration.module.ts b/apps/server/src/infra/identity-management/keycloak-administration/keycloak-administration.module.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-administration/keycloak-administration.module.ts rename to apps/server/src/infra/identity-management/keycloak-administration/keycloak-administration.module.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-administration/keycloak-config.ts b/apps/server/src/infra/identity-management/keycloak-administration/keycloak-config.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-administration/keycloak-config.ts rename to apps/server/src/infra/identity-management/keycloak-administration/keycloak-config.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service.spec.ts rename to apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts b/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts rename to apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts similarity index 98% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts index 16c5c9c2d1a..9b3120b8b8c 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { LegacyLogger } from '@src/core/logger'; import { KeycloakConfigurationUc } from '../uc/keycloak-configuration.uc'; import { KeycloakConsole } from './keycloak-configuration.console'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts b/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts similarity index 98% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts index 1d597e7020a..85d3f7a5a3c 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts @@ -1,4 +1,4 @@ -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { LegacyLogger } from '@src/core/logger'; import { Command, CommandOption, Console } from 'nestjs-console'; import { KeycloakConfigurationUc } from '../uc/keycloak-configuration.uc'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.ts b/apps/server/src/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts b/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts b/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/interface/keycloak-configuration-input-files.interface.ts b/apps/server/src/infra/identity-management/keycloak-configuration/interface/keycloak-configuration-input-files.interface.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/interface/keycloak-configuration-input-files.interface.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/interface/keycloak-configuration-input-files.interface.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-config.ts b/apps/server/src/infra/identity-management/keycloak-configuration/keycloak-config.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-config.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/keycloak-config.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts similarity index 94% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts index 3e31e5dcb08..f1d0ee3808e 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/keycloak-configuration.module.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ConfigModule } from '@nestjs/config'; import { KeycloakConfigurationModule } from './keycloak-configuration.module'; import { KeycloakConsole } from './console/keycloak-configuration.console'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts b/apps/server/src/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts similarity index 93% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts index 2012dad00a5..4e570b30dcf 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { EncryptionModule } from '@shared/infra/encryption'; -import { ConsoleWriterModule } from '@shared/infra/console'; +import { EncryptionModule } from '@infra/encryption'; +import { ConsoleWriterModule } from '@infra/console'; import { AccountModule } from '@modules/account'; import { SystemModule } from '@modules/system'; import { KeycloakAdministrationModule } from '../keycloak-administration/keycloak-administration.module'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts similarity index 98% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts index b28d74ca3d5..94ef9c042a4 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { OidcConfigDto } from '@modules/system/service'; import { OidcIdentityProviderMapper } from './identity-provider.mapper'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts similarity index 92% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts index 75737263cac..6573ed35a5b 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts @@ -1,6 +1,6 @@ import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import { Inject } from '@nestjs/common'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { OidcConfigDto } from '@modules/system/service'; export class OidcIdentityProviderMapper { diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts similarity index 98% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts index ad5af6a1d75..98ed3552918 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts @@ -4,7 +4,7 @@ import AuthenticationExecutionExportRepresentation from '@keycloak/keycloak-admi import AuthenticationFlowRepresentation from '@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { SystemRepo } from '@shared/repo/system/system.repo'; import { systemFactory } from '@shared/testing/factory'; import { LoggerModule } from '@src/core/logger'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts similarity index 99% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts index 1388392995e..61e475fe3e3 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts @@ -10,7 +10,7 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { SymetricKeyEncryptionService } from '@shared/infra/encryption'; +import { SymetricKeyEncryptionService } from '@infra/encryption'; import { systemFactory } from '@shared/testing'; import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; import { SystemOidcService } from '@modules/system/service/system-oidc.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts similarity index 98% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts index baf86c58ccf..eba5e20fcd0 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.integration.spec.ts @@ -3,7 +3,7 @@ import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { Account } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { accountFactory, cleanupCollections } from '@shared/testing'; import { LoggerModule } from '@src/core/logger'; import { v1 } from 'uuid'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts similarity index 97% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts index 87f28a28d76..11858b56a2b 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts @@ -2,7 +2,7 @@ import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; import { faker } from '@faker-js/faker'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { LoggerModule } from '@src/core/logger'; import { v1 } from 'uuid'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.spec.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts b/apps/server/src/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts rename to apps/server/src/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak/keycloak.module.spec.ts b/apps/server/src/infra/identity-management/keycloak/keycloak.module.spec.ts similarity index 94% rename from apps/server/src/shared/infra/identity-management/keycloak/keycloak.module.spec.ts rename to apps/server/src/infra/identity-management/keycloak/keycloak.module.spec.ts index 6bed42efdd6..282d6dd0e40 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/keycloak.module.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak/keycloak.module.spec.ts @@ -1,6 +1,6 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { KeycloakModule } from './keycloak.module'; import { KeycloakIdentityManagementService } from './service/keycloak-identity-management.service'; import { KeycloakIdentityManagementOauthService } from './service/keycloak-identity-management-oauth.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/keycloak.module.ts b/apps/server/src/infra/identity-management/keycloak/keycloak.module.ts similarity index 92% rename from apps/server/src/shared/infra/identity-management/keycloak/keycloak.module.ts rename to apps/server/src/infra/identity-management/keycloak/keycloak.module.ts index 4f7407f80c9..c2fd27be29e 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/keycloak.module.ts +++ b/apps/server/src/infra/identity-management/keycloak/keycloak.module.ts @@ -1,6 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { EncryptionModule } from '@shared/infra/encryption'; +import { EncryptionModule } from '@infra/encryption'; import { LoggerModule } from '@src/core/logger'; import { KeycloakAdministrationModule } from '../keycloak-administration/keycloak-administration.module'; import { KeycloakIdentityManagementOauthService } from './service/keycloak-identity-management-oauth.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts similarity index 96% rename from apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts rename to apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts index 9e4cf5567c1..40456bbb184 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.integration.spec.ts @@ -1,7 +1,7 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { KeycloakModule } from '@shared/infra/identity-management/keycloak/keycloak.module'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { KeycloakModule } from '@infra/identity-management/keycloak/keycloak.module'; import { LoggerModule } from '@src/core/logger'; import { v1 } from 'uuid'; import { KeycloakAdministrationModule } from '../../keycloak-administration/keycloak-administration.module'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts similarity index 99% rename from apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts rename to apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts index f9c3745ce64..9ccd20b6f99 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts @@ -3,7 +3,7 @@ import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts similarity index 97% rename from apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts rename to apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts index 7e10179b2cd..9eab3a4f60a 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts @@ -1,7 +1,7 @@ import { HttpService } from '@nestjs/axios'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { OauthConfigDto } from '@modules/system/service'; import qs from 'qs'; import { lastValueFrom } from 'rxjs'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts similarity index 96% rename from apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts rename to apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts index fd66603f730..c5f83c35f17 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts @@ -4,8 +4,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpModule } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { IdmAccount, IdmAccountUpdate } from '@shared/domain'; -import { KeycloakAdministrationService } from '@shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service'; -import { KeycloakModule } from '@shared/infra/identity-management/keycloak/keycloak.module'; +import { KeycloakAdministrationService } from '@infra/identity-management/keycloak-administration/service/keycloak-administration.service'; +import { KeycloakModule } from '@infra/identity-management/keycloak/keycloak.module'; import { ServerModule } from '@modules/server'; import { v1 } from 'uuid'; import { IdentityManagementService } from '../../identity-management.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.spec.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.spec.ts rename to apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.spec.ts diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.ts similarity index 100% rename from apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.ts rename to apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management.service.ts diff --git a/apps/server/src/shared/infra/index.ts b/apps/server/src/infra/index.ts similarity index 100% rename from apps/server/src/shared/infra/index.ts rename to apps/server/src/infra/index.ts diff --git a/apps/server/src/shared/infra/mail/index.ts b/apps/server/src/infra/mail/index.ts similarity index 100% rename from apps/server/src/shared/infra/mail/index.ts rename to apps/server/src/infra/mail/index.ts diff --git a/apps/server/src/shared/infra/mail/mail.interface.ts b/apps/server/src/infra/mail/mail.interface.ts similarity index 100% rename from apps/server/src/shared/infra/mail/mail.interface.ts rename to apps/server/src/infra/mail/mail.interface.ts diff --git a/apps/server/src/shared/infra/mail/mail.module.spec.ts b/apps/server/src/infra/mail/mail.module.spec.ts similarity index 90% rename from apps/server/src/shared/infra/mail/mail.module.spec.ts rename to apps/server/src/infra/mail/mail.module.spec.ts index 09cd1b6a9cc..3514b0e1043 100644 --- a/apps/server/src/shared/infra/mail/mail.module.spec.ts +++ b/apps/server/src/infra/mail/mail.module.spec.ts @@ -1,6 +1,6 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { Test, TestingModule } from '@nestjs/testing'; -import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq/rabbitmq.module'; +import { RabbitMQWrapperTestModule } from '@infra/rabbitmq'; import { MailModule } from './mail.module'; import { MailService } from './mail.service'; diff --git a/apps/server/src/shared/infra/mail/mail.module.ts b/apps/server/src/infra/mail/mail.module.ts similarity index 100% rename from apps/server/src/shared/infra/mail/mail.module.ts rename to apps/server/src/infra/mail/mail.module.ts diff --git a/apps/server/src/shared/infra/mail/mail.service.spec.ts b/apps/server/src/infra/mail/mail.service.spec.ts similarity index 100% rename from apps/server/src/shared/infra/mail/mail.service.spec.ts rename to apps/server/src/infra/mail/mail.service.spec.ts diff --git a/apps/server/src/shared/infra/mail/mail.service.ts b/apps/server/src/infra/mail/mail.service.ts similarity index 100% rename from apps/server/src/shared/infra/mail/mail.service.ts rename to apps/server/src/infra/mail/mail.service.ts diff --git a/apps/server/src/shared/infra/metrics/index.ts b/apps/server/src/infra/metrics/index.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/index.ts rename to apps/server/src/infra/metrics/index.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/app.spec.ts b/apps/server/src/infra/metrics/prometheus/app.spec.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/app.spec.ts rename to apps/server/src/infra/metrics/prometheus/app.spec.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/app.ts b/apps/server/src/infra/metrics/prometheus/app.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/app.ts rename to apps/server/src/infra/metrics/prometheus/app.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/config.spec.ts b/apps/server/src/infra/metrics/prometheus/config.spec.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/config.spec.ts rename to apps/server/src/infra/metrics/prometheus/config.spec.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/config.ts b/apps/server/src/infra/metrics/prometheus/config.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/config.ts rename to apps/server/src/infra/metrics/prometheus/config.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/handler.spec.ts b/apps/server/src/infra/metrics/prometheus/handler.spec.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/handler.spec.ts rename to apps/server/src/infra/metrics/prometheus/handler.spec.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/handler.ts b/apps/server/src/infra/metrics/prometheus/handler.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/handler.ts rename to apps/server/src/infra/metrics/prometheus/handler.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/index.ts b/apps/server/src/infra/metrics/prometheus/index.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/index.ts rename to apps/server/src/infra/metrics/prometheus/index.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/middleware.spec.ts b/apps/server/src/infra/metrics/prometheus/middleware.spec.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/middleware.spec.ts rename to apps/server/src/infra/metrics/prometheus/middleware.spec.ts diff --git a/apps/server/src/shared/infra/metrics/prometheus/middleware.ts b/apps/server/src/infra/metrics/prometheus/middleware.ts similarity index 100% rename from apps/server/src/shared/infra/metrics/prometheus/middleware.ts rename to apps/server/src/infra/metrics/prometheus/middleware.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/index.ts b/apps/server/src/infra/oauth-provider/dto/index.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/index.ts rename to apps/server/src/infra/oauth-provider/dto/index.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/interface/oauth-client.interface.ts b/apps/server/src/infra/oauth-provider/dto/interface/oauth-client.interface.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/interface/oauth-client.interface.ts rename to apps/server/src/infra/oauth-provider/dto/interface/oauth-client.interface.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/interface/oidc-context.interface.ts b/apps/server/src/infra/oauth-provider/dto/interface/oidc-context.interface.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/interface/oidc-context.interface.ts rename to apps/server/src/infra/oauth-provider/dto/interface/oidc-context.interface.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/request/accept-consent-request.body.ts b/apps/server/src/infra/oauth-provider/dto/request/accept-consent-request.body.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/request/accept-consent-request.body.ts rename to apps/server/src/infra/oauth-provider/dto/request/accept-consent-request.body.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/request/accept-login-request.body.ts b/apps/server/src/infra/oauth-provider/dto/request/accept-login-request.body.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/request/accept-login-request.body.ts rename to apps/server/src/infra/oauth-provider/dto/request/accept-login-request.body.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/request/reject-request.body.ts b/apps/server/src/infra/oauth-provider/dto/request/reject-request.body.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/request/reject-request.body.ts rename to apps/server/src/infra/oauth-provider/dto/request/reject-request.body.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/response/consent-session.response.ts b/apps/server/src/infra/oauth-provider/dto/response/consent-session.response.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/response/consent-session.response.ts rename to apps/server/src/infra/oauth-provider/dto/response/consent-session.response.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/response/consent.response.ts b/apps/server/src/infra/oauth-provider/dto/response/consent.response.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/response/consent.response.ts rename to apps/server/src/infra/oauth-provider/dto/response/consent.response.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/response/introspect.response.ts b/apps/server/src/infra/oauth-provider/dto/response/introspect.response.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/response/introspect.response.ts rename to apps/server/src/infra/oauth-provider/dto/response/introspect.response.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/response/login.response.ts b/apps/server/src/infra/oauth-provider/dto/response/login.response.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/response/login.response.ts rename to apps/server/src/infra/oauth-provider/dto/response/login.response.ts diff --git a/apps/server/src/shared/infra/oauth-provider/dto/response/redirect.response.ts b/apps/server/src/infra/oauth-provider/dto/response/redirect.response.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/dto/response/redirect.response.ts rename to apps/server/src/infra/oauth-provider/dto/response/redirect.response.ts diff --git a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts similarity index 98% rename from apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts rename to apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts index 0e244b37d16..7e30c5668c7 100644 --- a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts +++ b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts @@ -11,12 +11,12 @@ import { ProviderOauthClient, ProviderRedirectResponse, RejectRequestBody, -} from '@shared/infra/oauth-provider/dto'; -import { ProviderConsentSessionResponse } from '@shared/infra/oauth-provider/dto/response/consent-session.response'; -import { HydraAdapter } from '@shared/infra/oauth-provider/hydra/hydra.adapter'; +} from '@infra/oauth-provider/dto'; import { axiosResponseFactory } from '@shared/testing'; import { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'; import { of } from 'rxjs'; +import { HydraAdapter } from './hydra.adapter'; +import { ProviderConsentSessionResponse } from '../dto/response/consent-session.response'; import resetAllMocks = jest.resetAllMocks; class HydraAdapterSpec extends HydraAdapter { diff --git a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.ts b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.ts rename to apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts diff --git a/apps/server/src/shared/infra/oauth-provider/index.ts b/apps/server/src/infra/oauth-provider/index.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/index.ts rename to apps/server/src/infra/oauth-provider/index.ts diff --git a/apps/server/src/shared/infra/oauth-provider/oauth-provider-service.module.ts b/apps/server/src/infra/oauth-provider/oauth-provider-service.module.ts similarity index 61% rename from apps/server/src/shared/infra/oauth-provider/oauth-provider-service.module.ts rename to apps/server/src/infra/oauth-provider/oauth-provider-service.module.ts index 646ad228245..521f9216050 100644 --- a/apps/server/src/shared/infra/oauth-provider/oauth-provider-service.module.ts +++ b/apps/server/src/infra/oauth-provider/oauth-provider-service.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { OauthProviderService } from '@shared/infra/oauth-provider/oauth-provider.service'; -import { HydraAdapter } from '@shared/infra/oauth-provider/hydra/hydra.adapter'; import { HttpModule } from '@nestjs/axios'; +import { OauthProviderService } from './oauth-provider.service'; +import { HydraAdapter } from './hydra/hydra.adapter'; @Module({ imports: [HttpModule], diff --git a/apps/server/src/shared/infra/oauth-provider/oauth-provider.service.ts b/apps/server/src/infra/oauth-provider/oauth-provider.service.ts similarity index 100% rename from apps/server/src/shared/infra/oauth-provider/oauth-provider.service.ts rename to apps/server/src/infra/oauth-provider/oauth-provider.service.ts diff --git a/apps/server/src/shared/infra/preview-generator/index.ts b/apps/server/src/infra/preview-generator/index.ts similarity index 97% rename from apps/server/src/shared/infra/preview-generator/index.ts rename to apps/server/src/infra/preview-generator/index.ts index 570b38cc9bd..c5d413789be 100644 --- a/apps/server/src/shared/infra/preview-generator/index.ts +++ b/apps/server/src/infra/preview-generator/index.ts @@ -1,4 +1,4 @@ -export * from './interface'; -export * from './preview-generator-consumer.module'; -export * from './preview-generator-producer.module'; -export * from './preview.producer'; +export * from './interface'; +export * from './preview-generator-consumer.module'; +export * from './preview-generator-producer.module'; +export * from './preview.producer'; diff --git a/apps/server/src/shared/infra/preview-generator/interface/index.ts b/apps/server/src/infra/preview-generator/interface/index.ts similarity index 96% rename from apps/server/src/shared/infra/preview-generator/interface/index.ts rename to apps/server/src/infra/preview-generator/interface/index.ts index 37aae418ee2..45799160cd5 100644 --- a/apps/server/src/shared/infra/preview-generator/interface/index.ts +++ b/apps/server/src/infra/preview-generator/interface/index.ts @@ -1 +1 @@ -export * from './preview'; +export * from './preview'; diff --git a/apps/server/src/shared/infra/preview-generator/interface/preview-consumer-config.ts b/apps/server/src/infra/preview-generator/interface/preview-consumer-config.ts similarity index 79% rename from apps/server/src/shared/infra/preview-generator/interface/preview-consumer-config.ts rename to apps/server/src/infra/preview-generator/interface/preview-consumer-config.ts index 2924fc945bc..3e08b89075c 100644 --- a/apps/server/src/shared/infra/preview-generator/interface/preview-consumer-config.ts +++ b/apps/server/src/infra/preview-generator/interface/preview-consumer-config.ts @@ -1,4 +1,4 @@ -import { S3Config } from '@shared/infra/s3-client'; +import { S3Config } from '@infra/s3-client'; export interface PreviewModuleConfig { NEST_LOG_LEVEL: string; diff --git a/apps/server/src/shared/infra/preview-generator/interface/preview.ts b/apps/server/src/infra/preview-generator/interface/preview.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/interface/preview.ts rename to apps/server/src/infra/preview-generator/interface/preview.ts diff --git a/apps/server/src/shared/infra/preview-generator/loggable/preview-actions.loggable.spec.ts b/apps/server/src/infra/preview-generator/loggable/preview-actions.loggable.spec.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/loggable/preview-actions.loggable.spec.ts rename to apps/server/src/infra/preview-generator/loggable/preview-actions.loggable.spec.ts diff --git a/apps/server/src/shared/infra/preview-generator/loggable/preview-actions.loggable.ts b/apps/server/src/infra/preview-generator/loggable/preview-actions.loggable.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/loggable/preview-actions.loggable.ts rename to apps/server/src/infra/preview-generator/loggable/preview-actions.loggable.ts diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator-consumer.module.ts b/apps/server/src/infra/preview-generator/preview-generator-consumer.module.ts similarity index 89% rename from apps/server/src/shared/infra/preview-generator/preview-generator-consumer.module.ts rename to apps/server/src/infra/preview-generator/preview-generator-consumer.module.ts index 9d352b81d9d..ca4df0d074c 100644 --- a/apps/server/src/shared/infra/preview-generator/preview-generator-consumer.module.ts +++ b/apps/server/src/infra/preview-generator/preview-generator-consumer.module.ts @@ -1,7 +1,7 @@ import { DynamicModule, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { RabbitMQWrapperModule } from '@shared/infra/rabbitmq'; -import { S3ClientAdapter, S3ClientModule } from '@shared/infra/s3-client'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; +import { S3ClientAdapter, S3ClientModule } from '@infra/s3-client'; import { createConfigModuleOptions } from '@src/config'; import { Logger, LoggerModule } from '@src/core/logger'; import { PreviewConfig } from './interface/preview-consumer-config'; diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator-producer.module.ts b/apps/server/src/infra/preview-generator/preview-generator-producer.module.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/preview-generator-producer.module.ts rename to apps/server/src/infra/preview-generator/preview-generator-producer.module.ts diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator.builder.spec.ts b/apps/server/src/infra/preview-generator/preview-generator.builder.spec.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/preview-generator.builder.spec.ts rename to apps/server/src/infra/preview-generator/preview-generator.builder.spec.ts diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator.builder.ts b/apps/server/src/infra/preview-generator/preview-generator.builder.ts similarity index 87% rename from apps/server/src/shared/infra/preview-generator/preview-generator.builder.ts rename to apps/server/src/infra/preview-generator/preview-generator.builder.ts index 4c5561ed089..088f9e7ab08 100644 --- a/apps/server/src/shared/infra/preview-generator/preview-generator.builder.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.builder.ts @@ -1,4 +1,4 @@ -import { File } from '@shared/infra/s3-client'; +import { File } from '@infra/s3-client'; import { PassThrough } from 'stream'; import { PreviewOptions } from './interface'; diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator.consumer.spec.ts b/apps/server/src/infra/preview-generator/preview-generator.consumer.spec.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/preview-generator.consumer.spec.ts rename to apps/server/src/infra/preview-generator/preview-generator.consumer.spec.ts diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator.consumer.ts b/apps/server/src/infra/preview-generator/preview-generator.consumer.ts similarity index 92% rename from apps/server/src/shared/infra/preview-generator/preview-generator.consumer.ts rename to apps/server/src/infra/preview-generator/preview-generator.consumer.ts index 8fc08d261f3..d34fc8bc37c 100644 --- a/apps/server/src/shared/infra/preview-generator/preview-generator.consumer.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.consumer.ts @@ -1,7 +1,7 @@ import { RabbitPayload, RabbitRPC } from '@golevelup/nestjs-rabbitmq'; import { Injectable } from '@nestjs/common'; import { Logger } from '@src/core/logger'; -import { FilesPreviewEvents, FilesPreviewExchange } from '@src/shared/infra/rabbitmq'; +import { FilesPreviewEvents, FilesPreviewExchange } from '@infra/rabbitmq'; import { PreviewFileOptions } from './interface'; import { PreviewActionsLoggable } from './loggable/preview-actions.loggable'; import { PreviewGeneratorService } from './preview-generator.service'; diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator.service.spec.ts b/apps/server/src/infra/preview-generator/preview-generator.service.spec.ts similarity index 98% rename from apps/server/src/shared/infra/preview-generator/preview-generator.service.spec.ts rename to apps/server/src/infra/preview-generator/preview-generator.service.spec.ts index b8eeea612f5..016c261b122 100644 --- a/apps/server/src/shared/infra/preview-generator/preview-generator.service.spec.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.service.spec.ts @@ -1,6 +1,6 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { GetFile, S3ClientAdapter } from '@shared/infra/s3-client'; +import { GetFile, S3ClientAdapter } from '@infra/s3-client'; import { Logger } from '@src/core/logger'; import { Readable } from 'node:stream'; import { PreviewGeneratorService } from './preview-generator.service'; diff --git a/apps/server/src/shared/infra/preview-generator/preview-generator.service.ts b/apps/server/src/infra/preview-generator/preview-generator.service.ts similarity index 96% rename from apps/server/src/shared/infra/preview-generator/preview-generator.service.ts rename to apps/server/src/infra/preview-generator/preview-generator.service.ts index 72dac25f076..83dca461a2f 100644 --- a/apps/server/src/shared/infra/preview-generator/preview-generator.service.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { GetFile, S3ClientAdapter } from '@shared/infra/s3-client'; +import { GetFile, S3ClientAdapter } from '@infra/s3-client'; import { Logger } from '@src/core/logger'; import { subClass } from 'gm'; import { PassThrough } from 'stream'; diff --git a/apps/server/src/shared/infra/preview-generator/preview.producer.spec.ts b/apps/server/src/infra/preview-generator/preview.producer.spec.ts similarity index 100% rename from apps/server/src/shared/infra/preview-generator/preview.producer.spec.ts rename to apps/server/src/infra/preview-generator/preview.producer.spec.ts diff --git a/apps/server/src/shared/infra/preview-generator/preview.producer.ts b/apps/server/src/infra/preview-generator/preview.producer.ts similarity index 97% rename from apps/server/src/shared/infra/preview-generator/preview.producer.ts rename to apps/server/src/infra/preview-generator/preview.producer.ts index 602e2503185..28cf6930830 100644 --- a/apps/server/src/shared/infra/preview-generator/preview.producer.ts +++ b/apps/server/src/infra/preview-generator/preview.producer.ts @@ -1,7 +1,7 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { FilesPreviewEvents, FilesPreviewExchange, RpcMessageProducer } from '@shared/infra/rabbitmq'; +import { FilesPreviewEvents, FilesPreviewExchange, RpcMessageProducer } from '@infra/rabbitmq'; import { Logger } from '@src/core/logger'; import { PreviewFileOptions, PreviewResponseMessage } from './interface'; import { PreviewModuleConfig } from './interface/preview-consumer-config'; diff --git a/apps/server/src/shared/infra/rabbitmq/error.mapper.spec.ts b/apps/server/src/infra/rabbitmq/error.mapper.spec.ts similarity index 97% rename from apps/server/src/shared/infra/rabbitmq/error.mapper.spec.ts rename to apps/server/src/infra/rabbitmq/error.mapper.spec.ts index c3cf506e541..884dfb35158 100644 --- a/apps/server/src/shared/infra/rabbitmq/error.mapper.spec.ts +++ b/apps/server/src/infra/rabbitmq/error.mapper.spec.ts @@ -4,7 +4,7 @@ import { ForbiddenException, InternalServerErrorException, } from '@nestjs/common'; -import { IError } from '@shared/infra/rabbitmq'; +import { IError } from '@infra/rabbitmq'; import _ from 'lodash'; import { ErrorMapper } from './error.mapper'; diff --git a/apps/server/src/shared/infra/rabbitmq/error.mapper.ts b/apps/server/src/infra/rabbitmq/error.mapper.ts similarity index 94% rename from apps/server/src/shared/infra/rabbitmq/error.mapper.ts rename to apps/server/src/infra/rabbitmq/error.mapper.ts index 60f2e73795e..6f7083d3ad9 100644 --- a/apps/server/src/shared/infra/rabbitmq/error.mapper.ts +++ b/apps/server/src/infra/rabbitmq/error.mapper.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { ErrorUtils } from '@src/core/error/utils'; -import { IError } from '@shared/infra/rabbitmq'; +import { IError } from '@infra/rabbitmq'; export class ErrorMapper { static mapRpcErrorResponseToDomainError( diff --git a/apps/server/src/shared/infra/rabbitmq/exchange/files-preview.ts b/apps/server/src/infra/rabbitmq/exchange/files-preview.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/exchange/files-preview.ts rename to apps/server/src/infra/rabbitmq/exchange/files-preview.ts diff --git a/apps/server/src/shared/infra/rabbitmq/exchange/files-storage.ts b/apps/server/src/infra/rabbitmq/exchange/files-storage.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/exchange/files-storage.ts rename to apps/server/src/infra/rabbitmq/exchange/files-storage.ts diff --git a/apps/server/src/shared/infra/rabbitmq/exchange/index.ts b/apps/server/src/infra/rabbitmq/exchange/index.ts similarity index 97% rename from apps/server/src/shared/infra/rabbitmq/exchange/index.ts rename to apps/server/src/infra/rabbitmq/exchange/index.ts index 0cf6bd00d13..48658a3f0a7 100644 --- a/apps/server/src/shared/infra/rabbitmq/exchange/index.ts +++ b/apps/server/src/infra/rabbitmq/exchange/index.ts @@ -1,2 +1,2 @@ -export * from './files-preview'; -export * from './files-storage'; +export * from './files-preview'; +export * from './files-storage'; diff --git a/apps/server/src/shared/infra/rabbitmq/index.ts b/apps/server/src/infra/rabbitmq/index.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/index.ts rename to apps/server/src/infra/rabbitmq/index.ts diff --git a/apps/server/src/shared/infra/rabbitmq/rabbitmq.module.ts b/apps/server/src/infra/rabbitmq/rabbitmq.module.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/rabbitmq.module.ts rename to apps/server/src/infra/rabbitmq/rabbitmq.module.ts diff --git a/apps/server/src/shared/infra/rabbitmq/rpc-message-producer.spec.ts b/apps/server/src/infra/rabbitmq/rpc-message-producer.spec.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/rpc-message-producer.spec.ts rename to apps/server/src/infra/rabbitmq/rpc-message-producer.spec.ts diff --git a/apps/server/src/shared/infra/rabbitmq/rpc-message-producer.ts b/apps/server/src/infra/rabbitmq/rpc-message-producer.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/rpc-message-producer.ts rename to apps/server/src/infra/rabbitmq/rpc-message-producer.ts diff --git a/apps/server/src/shared/infra/rabbitmq/rpc-message.ts b/apps/server/src/infra/rabbitmq/rpc-message.ts similarity index 100% rename from apps/server/src/shared/infra/rabbitmq/rpc-message.ts rename to apps/server/src/infra/rabbitmq/rpc-message.ts diff --git a/apps/server/src/shared/infra/redis/index.ts b/apps/server/src/infra/redis/index.ts similarity index 100% rename from apps/server/src/shared/infra/redis/index.ts rename to apps/server/src/infra/redis/index.ts diff --git a/apps/server/src/shared/infra/redis/interface/redis.constants.ts b/apps/server/src/infra/redis/interface/redis.constants.ts similarity index 100% rename from apps/server/src/shared/infra/redis/interface/redis.constants.ts rename to apps/server/src/infra/redis/interface/redis.constants.ts diff --git a/apps/server/src/shared/infra/redis/redis.module.ts b/apps/server/src/infra/redis/redis.module.ts similarity index 100% rename from apps/server/src/shared/infra/redis/redis.module.ts rename to apps/server/src/infra/redis/redis.module.ts diff --git a/apps/server/src/shared/infra/s3-client/README.md b/apps/server/src/infra/s3-client/README.md similarity index 100% rename from apps/server/src/shared/infra/s3-client/README.md rename to apps/server/src/infra/s3-client/README.md diff --git a/apps/server/src/shared/infra/s3-client/constants.ts b/apps/server/src/infra/s3-client/constants.ts similarity index 100% rename from apps/server/src/shared/infra/s3-client/constants.ts rename to apps/server/src/infra/s3-client/constants.ts diff --git a/apps/server/src/shared/infra/s3-client/index.ts b/apps/server/src/infra/s3-client/index.ts similarity index 97% rename from apps/server/src/shared/infra/s3-client/index.ts rename to apps/server/src/infra/s3-client/index.ts index a2bfb7428c3..89943618d9e 100644 --- a/apps/server/src/shared/infra/s3-client/index.ts +++ b/apps/server/src/infra/s3-client/index.ts @@ -1,3 +1,3 @@ -export * from './interface'; -export * from './s3-client.adapter'; -export * from './s3-client.module'; +export * from './interface'; +export * from './s3-client.adapter'; +export * from './s3-client.module'; diff --git a/apps/server/src/shared/infra/s3-client/interface/index.ts b/apps/server/src/infra/s3-client/interface/index.ts similarity index 100% rename from apps/server/src/shared/infra/s3-client/interface/index.ts rename to apps/server/src/infra/s3-client/interface/index.ts diff --git a/apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts b/apps/server/src/infra/s3-client/s3-client.adapter.spec.ts similarity index 99% rename from apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts rename to apps/server/src/infra/s3-client/s3-client.adapter.spec.ts index 87de7afb1c3..7986e692ca9 100644 --- a/apps/server/src/shared/infra/s3-client/s3-client.adapter.spec.ts +++ b/apps/server/src/infra/s3-client/s3-client.adapter.spec.ts @@ -6,9 +6,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ErrorUtils } from '@src/core/error/utils'; import { LegacyLogger } from '@src/core/logger'; import { Readable } from 'node:stream'; -import { FileDto } from '../../../modules/files-storage/dto'; import { S3_CLIENT, S3_CONFIG } from './constants'; -import { S3Config } from './interface'; +import { File, S3Config } from './interface'; import { S3ClientAdapter } from './s3-client.adapter'; const createParameter = () => { @@ -190,11 +189,10 @@ describe('S3ClientAdapter', () => { describe('create', () => { const createFile = () => { const readable = Readable.from('ddd'); - const file = new FileDto({ + const file: File = { data: readable, - name: 'test.txt', mimeType: 'text/plain', - }); + }; return { file }; }; diff --git a/apps/server/src/shared/infra/s3-client/s3-client.adapter.ts b/apps/server/src/infra/s3-client/s3-client.adapter.ts similarity index 100% rename from apps/server/src/shared/infra/s3-client/s3-client.adapter.ts rename to apps/server/src/infra/s3-client/s3-client.adapter.ts diff --git a/apps/server/src/shared/infra/s3-client/s3-client.module.spec.ts b/apps/server/src/infra/s3-client/s3-client.module.spec.ts similarity index 100% rename from apps/server/src/shared/infra/s3-client/s3-client.module.spec.ts rename to apps/server/src/infra/s3-client/s3-client.module.spec.ts diff --git a/apps/server/src/shared/infra/s3-client/s3-client.module.ts b/apps/server/src/infra/s3-client/s3-client.module.ts similarity index 100% rename from apps/server/src/shared/infra/s3-client/s3-client.module.ts rename to apps/server/src/infra/s3-client/s3-client.module.ts diff --git a/apps/server/src/modules/account/account.module.spec.ts b/apps/server/src/modules/account/account.module.spec.ts index 74a8d25568b..8acbff04090 100644 --- a/apps/server/src/modules/account/account.module.spec.ts +++ b/apps/server/src/modules/account/account.module.spec.ts @@ -1,6 +1,6 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { AccountModule } from './account.module'; import { AccountIdmToDtoMapper, AccountIdmToDtoMapperDb, AccountIdmToDtoMapperIdm } from './mapper'; import { AccountService } from './services/account.service'; diff --git a/apps/server/src/modules/account/account.module.ts b/apps/server/src/modules/account/account.module.ts index 6c98d7f76e3..2e11af11c6d 100644 --- a/apps/server/src/modules/account/account.module.ts +++ b/apps/server/src/modules/account/account.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PermissionService } from '@shared/domain'; import { SystemRepo, UserRepo } from '@shared/repo'; -import { IdentityManagementModule } from '@shared/infra/identity-management'; +import { IdentityManagementModule } from '@infra/identity-management'; import { LoggerModule } from '@src/core/logger/logger.module'; import { AccountRepo } from './repo/account.repo'; import { AccountService } from './services/account.service'; diff --git a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts index bf4a44119fe..77acc53a3b1 100644 --- a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts +++ b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, User } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { accountFactory, cleanupCollections, userFactory } from '@shared/testing'; import { AccountRepo } from './account.repo'; diff --git a/apps/server/src/modules/account/services/account-db.service.spec.ts b/apps/server/src/modules/account/services/account-db.service.spec.ts index 64075bcb40c..107273797f9 100644 --- a/apps/server/src/modules/account/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/services/account-db.service.spec.ts @@ -4,7 +4,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { Account, EntityId, Permission, Role, RoleName, SchoolEntity, User } from '@shared/domain'; -import { IdentityManagementService } from '@shared/infra/identity-management/identity-management.service'; +import { IdentityManagementService } from '@infra/identity-management/identity-management.service'; import { accountFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; import { AccountEntityToDtoMapper } from '@modules/account/mapper'; import { AccountDto } from '@modules/account/services/dto'; diff --git a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts index 4761bbd80ca..06ad0943c22 100644 --- a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts @@ -4,11 +4,10 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { IdmAccount } from '@shared/domain'; -import { KeycloakAdministrationService } from '@shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service'; +import { KeycloakAdministrationService } from '@infra/identity-management/keycloak-administration/service/keycloak-administration.service'; import { AccountSaveDto } from '@modules/account/services/dto'; import { LoggerModule } from '@src/core/logger'; -import { IdentityManagementModule } from '@shared/infra/identity-management'; -import { IdentityManagementService } from '../../../shared/infra/identity-management/identity-management.service'; +import { IdentityManagementModule, IdentityManagementService } from '@infra/identity-management'; import { AccountIdmToDtoMapper, AccountIdmToDtoMapperDb } from '../mapper'; import { AccountServiceIdm } from './account-idm.service'; import { AbstractAccountService } from './account.service.abstract'; diff --git a/apps/server/src/modules/account/services/account-idm.service.spec.ts b/apps/server/src/modules/account/services/account-idm.service.spec.ts index 4b997d1b3fe..9b8705089b3 100644 --- a/apps/server/src/modules/account/services/account-idm.service.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { IdmAccount } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { IdentityManagementOauthService, IdentityManagementService } from '@shared/infra/identity-management'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { IdentityManagementOauthService, IdentityManagementService } from '@infra/identity-management'; import { NotImplementedException } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { ConfigModule } from '@nestjs/config'; diff --git a/apps/server/src/modules/account/services/account-idm.service.ts b/apps/server/src/modules/account/services/account-idm.service.ts index 68bcfb42bae..2136326e038 100644 --- a/apps/server/src/modules/account/services/account-idm.service.ts +++ b/apps/server/src/modules/account/services/account-idm.service.ts @@ -2,7 +2,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable, NotImplementedException } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { Counted, EntityId, IdmAccount, IdmAccountUpdate } from '@shared/domain'; -import { IdentityManagementService, IdentityManagementOauthService } from '@shared/infra/identity-management'; +import { IdentityManagementService, IdentityManagementOauthService } from '@infra/identity-management'; import { LegacyLogger } from '@src/core/logger'; import { AccountIdmToDtoMapper } from '../mapper'; import { AbstractAccountService } from './account.service.abstract'; diff --git a/apps/server/src/modules/account/services/account-lookup.service.spec.ts b/apps/server/src/modules/account/services/account-lookup.service.spec.ts index cfef246d3e3..c3351a6b0c3 100644 --- a/apps/server/src/modules/account/services/account-lookup.service.spec.ts +++ b/apps/server/src/modules/account/services/account-lookup.service.spec.ts @@ -4,7 +4,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { IdmAccount } from '@shared/domain'; -import { IdentityManagementService } from '@shared/infra/identity-management'; +import { IdentityManagementService } from '@infra/identity-management'; import { AccountLookupService } from './account-lookup.service'; describe('AccountLookupService', () => { diff --git a/apps/server/src/modules/account/services/account-lookup.service.ts b/apps/server/src/modules/account/services/account-lookup.service.ts index b1549590c5b..ed67d03232d 100644 --- a/apps/server/src/modules/account/services/account-lookup.service.ts +++ b/apps/server/src/modules/account/services/account-lookup.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain'; -import { IdentityManagementService } from '@shared/infra/identity-management'; +import { IdentityManagementService } from '@infra/identity-management'; import { IServerConfig } from '@modules/server/server.config'; import { ObjectId } from 'bson'; diff --git a/apps/server/src/modules/account/services/account.service.integration.spec.ts b/apps/server/src/modules/account/services/account.service.integration.spec.ts index d001925000b..ccaf450ffed 100644 --- a/apps/server/src/modules/account/services/account.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account.service.integration.spec.ts @@ -4,10 +4,10 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, IdmAccount } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { IdentityManagementModule } from '@shared/infra/identity-management'; -import { IdentityManagementService } from '@shared/infra/identity-management/identity-management.service'; -import { KeycloakAdministrationService } from '@shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { IdentityManagementModule } from '@infra/identity-management'; +import { IdentityManagementService } from '@infra/identity-management/identity-management.service'; +import { KeycloakAdministrationService } from '@infra/identity-management/keycloak-administration/service/keycloak-administration.service'; import { UserRepo } from '@shared/repo'; import { accountFactory, cleanupCollections } from '@shared/testing'; import { ObjectId } from 'bson'; diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/server/src/modules/authentication/authentication.module.ts index 26d20a4dfc8..8f2bdcd3b0d 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/server/src/modules/authentication/authentication.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; -import { CacheWrapperModule } from '@shared/infra/cache'; -import { IdentityManagementModule } from '@shared/infra/identity-management'; +import { CacheWrapperModule } from '@infra/cache'; +import { IdentityManagementModule } from '@infra/identity-management'; import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { AccountModule } from '@modules/account'; diff --git a/apps/server/src/modules/authentication/index.ts b/apps/server/src/modules/authentication/index.ts index 904c64ff97b..59e749c7abc 100644 --- a/apps/server/src/modules/authentication/index.ts +++ b/apps/server/src/modules/authentication/index.ts @@ -1,2 +1,3 @@ export { ICurrentUser } from './interface'; export { JWT, CurrentUser, Authenticate } from './decorator'; +export { AuthenticationModule } from './authentication.module'; diff --git a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts index 936deb866e4..6638e0470b2 100644 --- a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts +++ b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Test, TestingModule } from '@nestjs/testing'; -import { CacheService } from '@shared/infra/cache'; -import { CacheStoreType } from '@shared/infra/cache/interface/cache-store-type.enum'; +import { CacheService } from '@infra/cache'; +import { CacheStoreType } from '@infra/cache/interface/cache-store-type.enum'; import { feathersRedis } from '@src/imports-from-feathers'; import { Cache } from 'cache-manager'; import { JwtValidationAdapter } from './jwt-validation.adapter'; diff --git a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts index 3af5db2061b..dee98747c46 100644 --- a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts +++ b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts @@ -1,7 +1,7 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable } from '@nestjs/common'; -import { CacheService } from '@shared/infra/cache'; -import { CacheStoreType } from '@shared/infra/cache/interface/cache-store-type.enum'; +import { CacheService } from '@infra/cache'; +import { CacheStoreType } from '@infra/cache/interface/cache-store-type.enum'; import { addTokenToWhitelist, createRedisIdentifierFromJwtData, diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts index d1330270fb7..121d2874fe9 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { RoleName, User } from '@shared/domain'; -import { IdentityManagementOauthService } from '@shared/infra/identity-management'; +import { IdentityManagementOauthService } from '@infra/identity-management'; import { UserRepo } from '@shared/repo'; import { accountFactory, setupEntities, userFactory } from '@shared/testing'; import { AccountEntityToDtoMapper } from '@modules/account/mapper'; diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.ts b/apps/server/src/modules/authentication/strategy/local.strategy.ts index 7963a5166e7..28ed573b45e 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.ts @@ -6,7 +6,7 @@ import bcrypt from 'bcryptjs'; import { UserRepo } from '@shared/repo'; import { AccountDto } from '@modules/account/services/dto'; import { GuardAgainst } from '@shared/common/utils/guard-against'; -import { IdentityManagementOauthService, IIdentityManagementConfig } from '@shared/infra/identity-management'; +import { IdentityManagementOauthService, IIdentityManagementConfig } from '@infra/identity-management'; import { CurrentUserMapper } from '../mapper'; import { ICurrentUser } from '../interface'; import { AuthenticationService } from '../services/authentication.service'; diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index c555f13dc7b..d01cd9363f4 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { FeathersModule } from '@shared/infra/feathers'; +import { FeathersModule } from '@infra/feathers'; import { BoardDoRule, ContextExternalToolRule, diff --git a/apps/server/src/modules/authorization/feathers/feathers-auth.provider.spec.ts b/apps/server/src/modules/authorization/feathers/feathers-auth.provider.spec.ts index ccf1f2177aa..f035fa5a867 100644 --- a/apps/server/src/modules/authorization/feathers/feathers-auth.provider.spec.ts +++ b/apps/server/src/modules/authorization/feathers/feathers-auth.provider.spec.ts @@ -2,7 +2,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { Test, TestingModule } from '@nestjs/testing'; import { NewsTargetModel } from '@shared/domain'; -import { FeathersServiceProvider } from '@shared/infra/feathers'; +import { FeathersServiceProvider } from '@infra/feathers'; import { FeathersAuthProvider } from './feathers-auth.provider'; describe('FeathersAuthProvider', () => { diff --git a/apps/server/src/modules/authorization/feathers/feathers-auth.provider.ts b/apps/server/src/modules/authorization/feathers/feathers-auth.provider.ts index 1f3f40886eb..4e4f87c56d1 100644 --- a/apps/server/src/modules/authorization/feathers/feathers-auth.provider.ts +++ b/apps/server/src/modules/authorization/feathers/feathers-auth.provider.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { BaseEntity, EntityId, NewsTargetModel } from '@shared/domain'; import { ObjectId } from '@mikro-orm/mongodb'; -import { FeathersServiceProvider } from '@shared/infra/feathers'; +import { FeathersServiceProvider } from '@infra/feathers'; interface User { _id: ObjectId; diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 7722326a21d..ffa1e7ad580 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -3,7 +3,7 @@ import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { ContentElementFactory } from '@shared/domain'; -import { ConsoleWriterModule } from '@shared/infra/console'; +import { ConsoleWriterModule } from '@infra/console'; import { CourseRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { BoardDoRepo, BoardNodeRepo, RecursiveDeleteVisitor } from './repo'; diff --git a/apps/server/src/modules/board/repo/board-do.repo.spec.ts b/apps/server/src/modules/board/repo/board-do.repo.spec.ts index 3874e9301ba..2f9a6633e69 100644 --- a/apps/server/src/modules/board/repo/board-do.repo.spec.ts +++ b/apps/server/src/modules/board/repo/board-do.repo.spec.ts @@ -14,7 +14,7 @@ import { ColumnBoard, RichTextElementNode, } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cardFactory, cardNodeFactory, diff --git a/apps/server/src/modules/board/repo/board-node.repo.spec.ts b/apps/server/src/modules/board/repo/board-node.repo.spec.ts index 656d751f01d..2ebed80d0af 100644 --- a/apps/server/src/modules/board/repo/board-node.repo.spec.ts +++ b/apps/server/src/modules/board/repo/board-node.repo.spec.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { ColumnBoardNode } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cardNodeFactory, cleanupCollections, diff --git a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts b/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts index 6236d5de8bb..4b0688f0a0d 100644 --- a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts +++ b/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts @@ -3,7 +3,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; import { Test, TestingModule } from '@nestjs/testing'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { columnBoardFactory, columnFactory, diff --git a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts index d7c71352166..40a62ede2ed 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts @@ -18,7 +18,7 @@ import { RichTextElement, SubmissionContainerElement, } from '@shared/domain'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { cardFactory, columnBoardFactory, diff --git a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts index ba76693bb93..137f189319c 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts @@ -12,7 +12,7 @@ import { SubmissionItem, } from '@shared/domain'; import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { ObjectId } from 'bson'; import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; diff --git a/apps/server/src/modules/board/uc/board-management.uc.spec.ts b/apps/server/src/modules/board/uc/board-management.uc.spec.ts index 7c3464a8a52..83948e0ae1e 100644 --- a/apps/server/src/modules/board/uc/board-management.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board-management.uc.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConsoleWriterService } from '@shared/infra/console'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { ConsoleWriterService } from '@infra/console'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { courseFactory } from '@shared/testing'; import { BoardManagementUc } from '@modules/management/uc/board-management.uc'; diff --git a/apps/server/src/modules/class/repo/classes.repo.spec.ts b/apps/server/src/modules/class/repo/classes.repo.spec.ts index 7801045aff0..df0191d24ef 100644 --- a/apps/server/src/modules/class/repo/classes.repo.spec.ts +++ b/apps/server/src/modules/class/repo/classes.repo.spec.ts @@ -4,7 +4,7 @@ import { Test } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing/testing-module'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { SchoolEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, schoolFactory } from '@shared/testing'; import { Class } from '../domain'; import { ClassEntity } from '../entity'; diff --git a/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts b/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts index eedf1b5638e..a654ff86bdc 100644 --- a/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts +++ b/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts @@ -1,14 +1,13 @@ import { Module } from '@nestjs/common'; -import { CollaborativeStorageAdapterModule } from '@shared/infra/collaborative-storage/collaborative-storage-adapter.module'; +import { CollaborativeStorageAdapterModule } from '@infra/collaborative-storage'; import { TeamsRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '@modules/authorization'; -import { TeamPermissionsMapper } from '@modules/collaborative-storage/mapper/team-permissions.mapper'; -import { TeamMapper } from '@modules/collaborative-storage/mapper/team.mapper'; -import { CollaborativeStorageService } from '@modules/collaborative-storage/services/collaborative-storage.service'; -import { RoleModule } from '@modules/role/role.module'; -import { CollaborativeStorageController } from './controller/collaborative-storage.controller'; -import { CollaborativeStorageUc } from './uc/collaborative-storage.uc'; +import { RoleModule } from '@modules/role'; +import { CollaborativeStorageService } from './services'; +import { TeamPermissionsMapper, TeamMapper } from './mapper'; +import { CollaborativeStorageController } from './controller'; +import { CollaborativeStorageUc } from './uc'; @Module({ imports: [CollaborativeStorageAdapterModule, AuthorizationModule, LoggerModule, RoleModule], diff --git a/apps/server/src/modules/collaborative-storage/controller/index.ts b/apps/server/src/modules/collaborative-storage/controller/index.ts new file mode 100644 index 00000000000..6e3d2c24555 --- /dev/null +++ b/apps/server/src/modules/collaborative-storage/controller/index.ts @@ -0,0 +1 @@ +export * from './collaborative-storage.controller'; diff --git a/apps/server/src/modules/collaborative-storage/index.ts b/apps/server/src/modules/collaborative-storage/index.ts index 55ed293fd82..6a36bd891cf 100644 --- a/apps/server/src/modules/collaborative-storage/index.ts +++ b/apps/server/src/modules/collaborative-storage/index.ts @@ -1,2 +1,2 @@ -export * from './collaborative-storage.module'; -export * from './services'; +export { CollaborativeStorageModule } from './collaborative-storage.module'; +export { CollaborativeStorageService, TeamDto, TeamPermissionsDto, TeamUserDto } from './services'; diff --git a/apps/server/src/modules/collaborative-storage/mapper/index.ts b/apps/server/src/modules/collaborative-storage/mapper/index.ts new file mode 100644 index 00000000000..df052363bc4 --- /dev/null +++ b/apps/server/src/modules/collaborative-storage/mapper/index.ts @@ -0,0 +1,2 @@ +export * from './team-permissions.mapper'; +export * from './team.mapper'; diff --git a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts index 4f95ae44a11..a1f8757f576 100644 --- a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts +++ b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts @@ -3,7 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName, TeamEntity } from '@shared/domain'; -import { CollaborativeStorageAdapter } from '@shared/infra/collaborative-storage'; +import { CollaborativeStorageAdapter } from '@infra/collaborative-storage'; import { TeamsRepo } from '@shared/repo'; import { setupEntities } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; diff --git a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts index f9807cf691c..8f32f0ffc5f 100644 --- a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts +++ b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; -import { CollaborativeStorageAdapter } from '@shared/infra/collaborative-storage'; +import { CollaborativeStorageAdapter } from '@infra/collaborative-storage'; import { TeamsRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; diff --git a/apps/server/src/modules/collaborative-storage/uc/index.ts b/apps/server/src/modules/collaborative-storage/uc/index.ts new file mode 100644 index 00000000000..b08c347bf88 --- /dev/null +++ b/apps/server/src/modules/collaborative-storage/uc/index.ts @@ -0,0 +1 @@ +export * from './collaborative-storage.uc'; diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts index 5bc151c3541..bba32408e84 100644 --- a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing/testing-module'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections } from '@shared/testing'; import { DeletionLogMapper } from './mapper'; import { DeletionLogEntity } from '../entity'; diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts index 579dc0ec403..c3018180218 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing/testing-module'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections } from '@shared/testing'; import { DeletionRequestMapper } from './mapper'; import { DeletionRequestRepo } from './deletion-request.repo'; diff --git a/apps/server/src/modules/files-storage-client/dto/file.dto.spec.ts b/apps/server/src/modules/files-storage-client/dto/file.dto.spec.ts index 509eee7f49f..0017e0aaa42 100644 --- a/apps/server/src/modules/files-storage-client/dto/file.dto.spec.ts +++ b/apps/server/src/modules/files-storage-client/dto/file.dto.spec.ts @@ -1,4 +1,4 @@ -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { FileDto } from './file.dto'; describe('FileDto', () => { diff --git a/apps/server/src/modules/files-storage-client/dto/file.dto.ts b/apps/server/src/modules/files-storage-client/dto/file.dto.ts index 5ac6e76181f..38d6daf4c3b 100644 --- a/apps/server/src/modules/files-storage-client/dto/file.dto.ts +++ b/apps/server/src/modules/files-storage-client/dto/file.dto.ts @@ -1,5 +1,5 @@ import { EntityId } from '@shared/domain'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { IFileDomainObjectProps } from '../interfaces'; export class FileDto { diff --git a/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts b/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts index 35513f27b02..f302e53e9a0 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts @@ -1,5 +1,5 @@ import { EntityId } from '@shared/domain'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; export interface IFileDomainObjectProps { id: EntityId; diff --git a/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts b/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts index d45df0aef3d..12a7898d9cf 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts @@ -1,5 +1,5 @@ import { EntityId } from '@shared/domain'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; export interface IFileRequestInfo { schoolId: EntityId; diff --git a/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.spec.ts b/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.spec.ts index 9cb8f09553f..ccce67a089a 100644 --- a/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.spec.ts +++ b/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.spec.ts @@ -1,5 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { lessonFactory, setupEntities, taskFactory } from '@shared/testing'; import { CopyFilesOfParentParamBuilder } from './copy-files-of-parent-param.builder'; import { FileParamBuilder } from './files-storage-param.builder'; diff --git a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts index 2a7d100ca86..f48d66dce48 100644 --- a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts +++ b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts @@ -1,4 +1,4 @@ -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { ICopyFileDomainObjectProps, IFileDomainObjectProps } from '../interfaces'; import { FilesStorageClientMapper } from './files-storage-client.mapper'; diff --git a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts index 2c73ec3824e..233e47fd4c8 100644 --- a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts +++ b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts @@ -1,5 +1,5 @@ import { LessonEntity, Submission, Task } from '@shared/domain'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { CopyFileDto, FileDto } from '../dto'; import { EntitiesWithFiles, ICopyFileDomainObjectProps, IFileDomainObjectProps } from '../interfaces'; diff --git a/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.spec.ts b/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.spec.ts index 72a639620f1..23ce4f7e175 100644 --- a/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.spec.ts +++ b/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.spec.ts @@ -1,4 +1,4 @@ -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { lessonFactory, setupEntities, taskFactory } from '@shared/testing'; import { FileParamBuilder } from './files-storage-param.builder'; diff --git a/apps/server/src/modules/files-storage-client/service/files-storage.producer.spec.ts b/apps/server/src/modules/files-storage-client/service/files-storage.producer.spec.ts index 7e4ed5a1c83..ef2ac9c9d1d 100644 --- a/apps/server/src/modules/files-storage-client/service/files-storage.producer.spec.ts +++ b/apps/server/src/modules/files-storage-client/service/files-storage.producer.spec.ts @@ -3,7 +3,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { ErrorMapper, FileRecordParentType, FilesStorageEvents, FilesStorageExchange } from '@shared/infra/rabbitmq'; +import { ErrorMapper, FileRecordParentType, FilesStorageEvents, FilesStorageExchange } from '@infra/rabbitmq'; import { setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FilesStorageProducer } from './files-storage.producer'; diff --git a/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts b/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts index ea049442df4..34927c01831 100644 --- a/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts +++ b/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts @@ -11,7 +11,7 @@ import { IFileDO, IFileRecordParams, RpcMessageProducer, -} from '@src/shared/infra/rabbitmq'; +} from '@infra/rabbitmq'; import { IFilesStorageClientConfig } from '../interfaces'; @Injectable() diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts index 22a7a11fb5b..4a966165633 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts @@ -6,8 +6,8 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, courseFactory, diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts index 0557843b8eb..6c1087ce371 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts @@ -6,8 +6,8 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, fileRecordFactory, diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts index e86b778e0ce..27661af51f8 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts @@ -6,8 +6,8 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts index e87aa5ddbe6..f63eef6ac68 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts @@ -6,9 +6,9 @@ import { ExecutionContext, INestApplication, NotFoundException, StreamableFile } import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { PreviewProducer } from '@shared/infra/preview-generator'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { PreviewProducer } from '@infra/preview-generator'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts index 496f399d41b..9e666437748 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts @@ -6,8 +6,8 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, fileRecordFactory, diff --git a/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts b/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts index 6555b7bd0f9..913a259e9c3 100644 --- a/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts +++ b/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { StringToBoolean } from '@shared/controller'; import { EntityId } from '@shared/domain'; -import { ScanResult } from '@shared/infra/antivirus'; +import { ScanResult } from '@infra/antivirus'; import { Allow, IsBoolean, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; import { FileRecordParentType } from '../../entity'; import { PreviewOutputMimeTypes, PreviewWidth } from '../../interface'; diff --git a/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts b/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts index aabefa60f16..fc500fedee1 100644 --- a/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts +++ b/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts @@ -2,9 +2,9 @@ import { RabbitPayload, RabbitRPC } from '@golevelup/nestjs-rabbitmq'; import { MikroORM, UseRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { RpcMessage } from '@shared/infra/rabbitmq/rpc-message'; +import { RpcMessage } from '@infra/rabbitmq/rpc-message'; import { LegacyLogger } from '@src/core/logger'; -import { FilesStorageEvents, FilesStorageExchange, ICopyFileDO, IFileDO } from '@src/shared/infra/rabbitmq'; +import { FilesStorageEvents, FilesStorageExchange, ICopyFileDO, IFileDO } from '@infra/rabbitmq'; import { FilesStorageMapper } from '../mapper'; import { FilesStorageService } from '../service/files-storage.service'; import { PreviewService } from '../service/preview.service'; diff --git a/apps/server/src/modules/files-storage/dto/file.dto.ts b/apps/server/src/modules/files-storage/dto/file.dto.ts index 9668ac3af72..ecdc3a73296 100644 --- a/apps/server/src/modules/files-storage/dto/file.dto.ts +++ b/apps/server/src/modules/files-storage/dto/file.dto.ts @@ -1,4 +1,4 @@ -import { File } from '@shared/infra/s3-client'; +import { File } from '@infra/s3-client'; import { Readable } from 'stream'; export class FileDto implements File { diff --git a/apps/server/src/modules/files-storage/files-preview-amqp.module.ts b/apps/server/src/modules/files-storage/files-preview-amqp.module.ts index 411a26e76d6..78a1aec0129 100644 --- a/apps/server/src/modules/files-storage/files-preview-amqp.module.ts +++ b/apps/server/src/modules/files-storage/files-preview-amqp.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { PreviewGeneratorConsumerModule } from '@shared/infra/preview-generator'; +import { PreviewGeneratorConsumerModule } from '@infra/preview-generator'; import { defaultConfig, s3Config } from './files-storage.config'; @Module({ diff --git a/apps/server/src/modules/files-storage/files-storage-test.module.ts b/apps/server/src/modules/files-storage/files-storage-test.module.ts index 6f3d865ebb2..f219c8bccac 100644 --- a/apps/server/src/modules/files-storage/files-storage-test.module.ts +++ b/apps/server/src/modules/files-storage/files-storage-test.module.ts @@ -1,11 +1,10 @@ import { DynamicModule, Module } from '@nestjs/common'; import { ALL_ENTITIES } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory-database/types'; -import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq/rabbitmq.module'; +import { MongoMemoryDatabaseModule, MongoDatabaseModuleOptions } from '@infra/database'; +import { RabbitMQWrapperTestModule } from '@infra/rabbitmq'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthenticationModule } from '@modules/authentication'; import { AuthorizationModule } from '@modules/authorization'; import { FileRecord } from './entity'; import { FilesStorageApiModule } from './files-storage-api.module'; diff --git a/apps/server/src/modules/files-storage/files-storage.config.ts b/apps/server/src/modules/files-storage/files-storage.config.ts index 7fac8ded763..985b07f0ef1 100644 --- a/apps/server/src/modules/files-storage/files-storage.config.ts +++ b/apps/server/src/modules/files-storage/files-storage.config.ts @@ -1,5 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import { S3Config } from '@shared/infra/s3-client'; +import { S3Config } from '@infra/s3-client'; import { ICoreModuleConfig } from '@src/core'; export const FILES_STORAGE_S3_CONNECTION = 'FILES_STORAGE_S3_CONNECTION'; diff --git a/apps/server/src/modules/files-storage/files-storage.module.ts b/apps/server/src/modules/files-storage/files-storage.module.ts index ccdaeb7f9fa..a7432172a21 100644 --- a/apps/server/src/modules/files-storage/files-storage.module.ts +++ b/apps/server/src/modules/files-storage/files-storage.module.ts @@ -4,17 +4,16 @@ import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain'; -import { AntivirusModule } from '@shared/infra/antivirus/antivirus.module'; -import { PreviewGeneratorProducerModule } from '@shared/infra/preview-generator'; -import { RabbitMQWrapperModule } from '@shared/infra/rabbitmq/rabbitmq.module'; -import { S3ClientModule } from '@shared/infra/s3-client'; +import { AntivirusModule } from '@infra/antivirus'; +import { PreviewGeneratorProducerModule } from '@infra/preview-generator'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; +import { S3ClientModule } from '@infra/s3-client'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; import { FileRecord, FileRecordSecurityCheck } from './entity'; import { config, s3Config } from './files-storage.config'; import { FileRecordRepo } from './repo'; -import { FilesStorageService } from './service/files-storage.service'; -import { PreviewService } from './service/preview.service'; +import { FilesStorageService, PreviewService } from './service'; const imports = [ LoggerModule, diff --git a/apps/server/src/modules/files-storage/helper/path.ts b/apps/server/src/modules/files-storage/helper/path.ts index 3ae81aef62d..f11a3ce7b2a 100644 --- a/apps/server/src/modules/files-storage/helper/path.ts +++ b/apps/server/src/modules/files-storage/helper/path.ts @@ -1,5 +1,5 @@ import { EntityId } from '@shared/domain'; -import { CopyFiles } from '@shared/infra/s3-client'; +import { CopyFiles } from '@infra/s3-client'; import { FileRecord } from '../entity'; import { ErrorType } from '../error'; diff --git a/apps/server/src/modules/files-storage/helper/test-helper.ts b/apps/server/src/modules/files-storage/helper/test-helper.ts index a66bec17de2..77671a2d552 100644 --- a/apps/server/src/modules/files-storage/helper/test-helper.ts +++ b/apps/server/src/modules/files-storage/helper/test-helper.ts @@ -1,4 +1,4 @@ -import { GetFile } from '@shared/infra/s3-client'; +import { GetFile } from '@infra/s3-client'; import { Readable } from 'stream'; import { GetFileResponse } from '../interface'; diff --git a/apps/server/src/modules/files-storage/mapper/file-response.builder.ts b/apps/server/src/modules/files-storage/mapper/file-response.builder.ts index 02344e4b3cb..7ad856deb97 100644 --- a/apps/server/src/modules/files-storage/mapper/file-response.builder.ts +++ b/apps/server/src/modules/files-storage/mapper/file-response.builder.ts @@ -1,4 +1,4 @@ -import { GetFile } from '@shared/infra/s3-client'; +import { GetFile } from '@infra/s3-client'; import { GetFileResponse } from '../interface'; export class FileResponseBuilder { diff --git a/apps/server/src/modules/files-storage/mapper/preview.builder.ts b/apps/server/src/modules/files-storage/mapper/preview.builder.ts index 83a16448a98..aea85be53d8 100644 --- a/apps/server/src/modules/files-storage/mapper/preview.builder.ts +++ b/apps/server/src/modules/files-storage/mapper/preview.builder.ts @@ -1,4 +1,4 @@ -import { PreviewFileOptions } from '@shared/infra/preview-generator'; +import { PreviewFileOptions } from '@infra/preview-generator'; import { PreviewParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { createPath, createPreviewFilePath, createPreviewNameHash, getFormat } from '../helper'; diff --git a/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts b/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts index 735359db012..d1dbe490c25 100644 --- a/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts +++ b/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { cleanupCollections, fileRecordFactory } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { FileRecord, FileRecordParentType } from '../entity'; import { FileRecordRepo } from './filerecord.repo'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts index 4ba05e540a8..51e2535e557 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts index 3705f93b51c..353b77837d9 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts index bcee168c2b9..4c5f08e39ef 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { NotAcceptableException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { GetFile, S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { GetFile, S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts index 95f7c2d204c..546ac842799 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams, SingleFileParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts index c82f96074f1..3b6dec255fa 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts index 8523c7388fd..eefd8176169 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ConflictException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import _ from 'lodash'; diff --git a/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts index 022e6a4bf0d..765de1077bd 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { readableStreamWithFileTypeFactory } from '@shared/testing/factory/readable-stream-with-file-type.factory'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/modules/files-storage/service/files-storage.service.ts b/apps/server/src/modules/files-storage/service/files-storage.service.ts index 209f1804d3e..6eb9e89ea96 100644 --- a/apps/server/src/modules/files-storage/service/files-storage.service.ts +++ b/apps/server/src/modules/files-storage/service/files-storage.service.ts @@ -8,8 +8,8 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Counted, EntityId } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import FileType from 'file-type-cjs/file-type-cjs-index'; import { PassThrough, Readable } from 'stream'; diff --git a/apps/server/src/modules/files-storage/service/index.ts b/apps/server/src/modules/files-storage/service/index.ts new file mode 100644 index 00000000000..f5f1eb61392 --- /dev/null +++ b/apps/server/src/modules/files-storage/service/index.ts @@ -0,0 +1,2 @@ +export * from './files-storage.service'; +export * from './preview.service'; diff --git a/apps/server/src/modules/files-storage/service/preview.service.spec.ts b/apps/server/src/modules/files-storage/service/preview.service.spec.ts index f02f48aee21..a5fed69ef51 100644 --- a/apps/server/src/modules/files-storage/service/preview.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/preview.service.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { PreviewProducer } from '@shared/infra/preview-generator'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { PreviewProducer } from '@infra/preview-generator'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/service/preview.service.ts b/apps/server/src/modules/files-storage/service/preview.service.ts index e27fbc0645a..0a9ba63e8e1 100644 --- a/apps/server/src/modules/files-storage/service/preview.service.ts +++ b/apps/server/src/modules/files-storage/service/preview.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { PreviewProducer } from '@shared/infra/preview-generator'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { PreviewProducer } from '@infra/preview-generator'; +import { S3ClientAdapter } from '@infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import { PreviewParams } from '../controller/dto'; import { FileRecord, PreviewStatus } from '../entity'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts index 2b7f1052121..cefaba3ac24 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts @@ -4,8 +4,8 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { Action } from '@modules/authorization'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts index b12006367aa..ef0dc16c1b6 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts @@ -5,8 +5,8 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Counted, EntityId } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts index f0aa9dcc25a..d34f004d73b 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts index 51ad0fd0b77..bcb5b2ec827 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts index 60d3fdd1a64..a4f3e0f8b2f 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts index b66c9c8821d..dc811c566a4 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts @@ -3,8 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts index c59f37d2599..5b126c8ea2a 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts index 43d9e9b7750..1125e7644bb 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts @@ -4,8 +4,8 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { AxiosHeadersKeyValue, axiosResponseFactory, fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { Action } from '@modules/authorization'; diff --git a/apps/server/src/modules/files/repo/files.repo.spec.ts b/apps/server/src/modules/files/repo/files.repo.spec.ts index ea33ae7917a..0ef8136918d 100644 --- a/apps/server/src/modules/files/repo/files.repo.spec.ts +++ b/apps/server/src/modules/files/repo/files.repo.spec.ts @@ -1,6 +1,6 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { StorageProviderEntity } from '@shared/domain'; import { FileEntity } from '../entity'; import { fileEntityFactory, filePermissionEntityFactory } from '../entity/testing'; diff --git a/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts b/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts index 9eee09a30af..1c9921dbff5 100644 --- a/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts +++ b/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts @@ -2,7 +2,7 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { INestApplication, NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { Readable } from 'stream'; import request from 'supertest'; diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts index 62e25bef4e2..d644ca2a57c 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts @@ -2,10 +2,10 @@ import { HttpModule } from '@nestjs/axios'; import { DynamicModule, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { Account, Role, SchoolEntity, SchoolYearEntity, SystemEntity, User } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory-database/types'; -import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; -import { S3ClientModule } from '@shared/infra/s3-client'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { MongoDatabaseModuleOptions } from '@infra/database/mongo-memory-database/types'; +import { RabbitMQWrapperTestModule } from '@infra/rabbitmq'; +import { S3ClientModule } from '@infra/s3-client'; import { createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts index 56ae93e0205..6cfcb03b74f 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts @@ -1,5 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import { S3Config } from '@shared/infra/s3-client'; +import { S3Config } from '@infra/s3-client'; export const FWU_CONTENT_S3_CONNECTION = 'FWU_CONTENT_S3_CONNECTION'; diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts index b15c8a04054..a991ca56503 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts @@ -4,8 +4,8 @@ import { HttpModule } from '@nestjs/axios'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { Account, Role, SchoolEntity, SchoolYearEntity, SystemEntity, User } from '@shared/domain'; -import { RabbitMQWrapperModule } from '@shared/infra/rabbitmq'; -import { S3ClientModule } from '@shared/infra/s3-client'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; +import { S3ClientModule } from '@infra/s3-client'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; diff --git a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts index 80240e0ea8e..e7606aeb245 100644 --- a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts +++ b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import { Readable } from 'stream'; import { FWU_CONTENT_S3_CONNECTION } from '../fwu-learning-contents.config'; diff --git a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts index afab92d46a5..6cdc20b9321 100644 --- a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts +++ b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import { FWU_CONTENT_S3_CONNECTION } from '../fwu-learning-contents.config'; diff --git a/apps/server/src/modules/group/repo/group.repo.spec.ts b/apps/server/src/modules/group/repo/group.repo.spec.ts index 358b3c13983..1bcc024514a 100644 --- a/apps/server/src/modules/group/repo/group.repo.spec.ts +++ b/apps/server/src/modules/group/repo/group.repo.spec.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { ExternalSource, SchoolEntity, UserDO, User } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, groupEntityFactory, diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts index 543db38ebbf..0c7251e6134 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-ajax.api.spec.ts @@ -3,7 +3,7 @@ import { H5PAjaxEndpoint } from '@lumieducation/h5p-server'; import { EntityManager } from '@mikro-orm/core'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { H5PEditorTestModule } from '../../h5p-editor-test.module'; import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts index 9f3b0017d08..e2af08f3fd5 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts @@ -3,7 +3,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts index 702aac1a717..05132888f71 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts @@ -4,7 +4,7 @@ import { ContentMetadata } from '@lumieducation/h5p-server/build/src/ContentMeta import { EntityManager } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { courseFactory, h5pContentFactory, diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts index 737266f300d..3f738fd67c0 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts @@ -3,7 +3,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts index 708dfef968a..6e98bb6905a 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts @@ -4,7 +4,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts index 9981a13e81f..0e1d5a13686 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-save-create.api.spec.ts @@ -3,7 +3,7 @@ import { IContentMetadata } from '@lumieducation/h5p-server'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { H5PContentParentType } from '../../entity'; import { H5PEditorTestModule } from '../../h5p-editor-test.module'; diff --git a/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts b/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts index 1ac4fc092e3..277c233dfa4 100644 --- a/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts +++ b/apps/server/src/modules/h5p-editor/controller/dto/h5p-file.dto.ts @@ -1,5 +1,5 @@ import { Readable } from 'stream'; -import { File } from '@shared/infra/s3-client'; +import { File } from '@infra/s3-client'; export class H5pFileDto implements File { constructor(file: H5pFileDto) { diff --git a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts index 62cd06dcf6e..49d53c57726 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts @@ -1,14 +1,14 @@ import { DynamicModule, Module } from '@nestjs/common'; import { ALL_ENTITIES } from '@shared/domain'; -import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; -import { S3ClientModule } from '@shared/infra/s3-client'; +import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; +import { RabbitMQWrapperTestModule } from '@infra/rabbitmq'; +import { S3ClientModule } from '@infra/s3-client'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthenticationModule } from '@modules/authentication'; import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { UserModule } from '@modules/user'; -import { AuthenticationApiModule } from '../authentication/authentication-api.module'; +import { AuthenticationApiModule } from '@modules/authentication/authentication-api.module'; import { H5PEditorModule } from './h5p-editor.module'; import { H5PContentRepo, LibraryRepo, TemporaryFileRepo } from './repo'; import { ContentStorage, LibraryStorage, TemporaryFileStorage } from './service'; diff --git a/apps/server/src/modules/h5p-editor/h5p-editor.config.ts b/apps/server/src/modules/h5p-editor/h5p-editor.config.ts index f02084aa4e5..9509cf66a76 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor.config.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor.config.ts @@ -1,5 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import { S3Config } from '@shared/infra/s3-client'; +import { S3Config } from '@infra/s3-client'; const h5pEditorConfig = { NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, diff --git a/apps/server/src/modules/h5p-editor/h5p-editor.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor.module.ts index 34efcedd69e..c80ff8bd6c0 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor.module.ts @@ -3,14 +3,14 @@ import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain'; -import { RabbitMQWrapperModule } from '@shared/infra/rabbitmq'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { Logger } from '@src/core/logger'; import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { UserModule } from '@modules/user'; -import { S3ClientModule } from '@shared/infra/s3-client'; -import { AuthenticationModule } from '../authentication/authentication.module'; +import { S3ClientModule } from '@infra/s3-client'; +import { AuthenticationModule } from '@modules/authentication'; import { H5PEditorController } from './controller/h5p-editor.controller'; import { H5PContent, InstalledLibrary, H5pEditorTempFile } from './entity'; import { config, s3ConfigContent, s3ConfigLibraries } from './h5p-editor.config'; diff --git a/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts b/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts index e06836ced99..f9672ffb3ce 100644 --- a/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts +++ b/apps/server/src/modules/h5p-editor/repo/h5p-content.repo.integration.spec.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, h5pContentFactory } from '@shared/testing'; import { H5PContent } from '../entity'; import { H5PContentRepo } from './h5p-content.repo'; diff --git a/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts b/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts index 78d219aa557..79bcde09fe9 100644 --- a/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts +++ b/apps/server/src/modules/h5p-editor/repo/library.repo.spec.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { cleanupCollections } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ILibraryMetadata } from '@lumieducation/h5p-server'; import { LibraryRepo } from './library.repo'; diff --git a/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts b/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts index ccaff625d66..e5e763b6216 100644 --- a/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts +++ b/apps/server/src/modules/h5p-editor/repo/temporary-file.repo.integration.spec.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, h5pTemporaryFileFactory } from '@shared/testing'; import { H5pEditorTempFile } from '../entity'; import { TemporaryFileRepo } from './temporary-file.repo'; diff --git a/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts index a02c8867cd0..df19f05ae21 100644 --- a/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts +++ b/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts @@ -4,7 +4,7 @@ import { IContentMetadata, ILibraryName, IUser, LibraryName } from '@lumieducati import { HttpException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { IEntity } from '@shared/domain'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectID } from 'bson'; import { Readable } from 'stream'; import { GetH5PFileResponse } from '../controller/dto'; diff --git a/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts b/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts index 95895b867c8..753f40201e3 100644 --- a/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts +++ b/apps/server/src/modules/h5p-editor/service/contentStorage.service.ts @@ -17,7 +17,7 @@ import { NotFoundException, UnprocessableEntityException, } from '@nestjs/common'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ErrorUtils } from '@src/core/error/utils'; import { Readable } from 'stream'; import { H5pFileDto } from '../controller/dto/h5p-file.dto'; diff --git a/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts index afde26d59b0..1b7910f057d 100644 --- a/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts +++ b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.spec.ts @@ -6,7 +6,7 @@ import { H5pError, ILibraryMetadata, ILibraryName } from '@lumieducation/h5p-ser import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { FileMetadata, InstalledLibrary } from '../entity/library.entity'; import { H5P_LIBRARIES_S3_CONNECTION } from '../h5p-editor.config'; import { LibraryRepo } from '../repo/library.repo'; diff --git a/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts index 6839a9d2a27..aff2b76ae16 100644 --- a/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts +++ b/apps/server/src/modules/h5p-editor/service/libraryStorage.service.ts @@ -10,7 +10,7 @@ import { type ILibraryStorage, } from '@lumieducation/h5p-server'; import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import mime from 'mime'; import path from 'node:path/posix'; import { Readable } from 'stream'; diff --git a/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts index bcbdfa9945c..b7d65e25cb4 100644 --- a/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts +++ b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.spec.ts @@ -2,7 +2,7 @@ import { ServiceOutputTypes } from '@aws-sdk/client-s3'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { IUser } from '@lumieducation/h5p-server'; import { Test, TestingModule } from '@nestjs/testing'; -import { File, S3ClientAdapter } from '@shared/infra/s3-client'; +import { File, S3ClientAdapter } from '@infra/s3-client'; import { ReadStream } from 'fs'; import { Readable } from 'node:stream'; import { GetH5pFileResponse } from '../controller/dto'; diff --git a/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts index 6bfa164e630..7921b52a27b 100644 --- a/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts +++ b/apps/server/src/modules/h5p-editor/service/temporary-file-storage.service.ts @@ -1,6 +1,6 @@ import { ITemporaryFile, ITemporaryFileStorage, IUser } from '@lumieducation/h5p-server'; import { Inject, Injectable, NotAcceptableException } from '@nestjs/common'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ReadStream } from 'fs'; import { Readable } from 'stream'; import { H5pFileDto } from '../controller/dto/h5p-file.dto'; diff --git a/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts b/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts index 64db859ddac..8a586c39eb6 100644 --- a/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { ColumnBoardTarget } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, columnBoardTargetFactory } from '@shared/testing'; import { ColumnBoardService } from '@modules/board'; import { ColumnBoardTargetService } from './column-board-target.service'; diff --git a/apps/server/src/modules/legacy-school/repo/schoolyear.repo.integration.spec.ts b/apps/server/src/modules/legacy-school/repo/schoolyear.repo.integration.spec.ts index 5439fdef9f6..1688e9e6d97 100644 --- a/apps/server/src/modules/legacy-school/repo/schoolyear.repo.integration.spec.ts +++ b/apps/server/src/modules/legacy-school/repo/schoolyear.repo.integration.spec.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolYearEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections } from '@shared/testing'; import { schoolYearFactory } from '@shared/testing/factory/schoolyear.factory'; import { SchoolYearRepo } from './schoolyear.repo'; diff --git a/apps/server/src/modules/lesson/lesson.module.ts b/apps/server/src/modules/lesson/lesson.module.ts index 2e246c63211..dde1eb157ec 100644 --- a/apps/server/src/modules/lesson/lesson.module.ts +++ b/apps/server/src/modules/lesson/lesson.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { FeathersServiceProvider } from '@shared/infra/feathers'; +import { FeathersServiceProvider } from '@infra/feathers'; import { LessonRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { CopyHelperModule } from '@modules/copy-helper'; diff --git a/apps/server/src/modules/lesson/service/etherpad.service.spec.ts b/apps/server/src/modules/lesson/service/etherpad.service.spec.ts index 81a903008c6..02bc68e67bf 100644 --- a/apps/server/src/modules/lesson/service/etherpad.service.spec.ts +++ b/apps/server/src/modules/lesson/service/etherpad.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain'; -import { FeathersServiceProvider } from '@shared/infra/feathers/feathers-service.provider'; +import { FeathersServiceProvider } from '@infra/feathers/feathers-service.provider'; import { LegacyLogger } from '@src/core/logger'; import { EtherpadService } from './etherpad.service'; diff --git a/apps/server/src/modules/lesson/service/etherpad.service.ts b/apps/server/src/modules/lesson/service/etherpad.service.ts index d630a93420e..62e0773071b 100644 --- a/apps/server/src/modules/lesson/service/etherpad.service.ts +++ b/apps/server/src/modules/lesson/service/etherpad.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { FeathersServiceProvider } from '@shared/infra/feathers/feathers-service.provider'; +import { FeathersServiceProvider } from '@infra/feathers'; import { LegacyLogger } from '@src/core/logger'; export type PadResponse = { data: { padID: string } }; diff --git a/apps/server/src/modules/lesson/service/nexboard.service.spec.ts b/apps/server/src/modules/lesson/service/nexboard.service.spec.ts index a4f40bd1989..8085a5fdd89 100644 --- a/apps/server/src/modules/lesson/service/nexboard.service.spec.ts +++ b/apps/server/src/modules/lesson/service/nexboard.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain'; -import { FeathersServiceProvider } from '@shared/infra/feathers/feathers-service.provider'; +import { FeathersServiceProvider } from '@infra/feathers'; import { LegacyLogger } from '@src/core/logger'; import { NexboardService } from './nexboard.service'; diff --git a/apps/server/src/modules/lesson/service/nexboard.service.ts b/apps/server/src/modules/lesson/service/nexboard.service.ts index 01ca20647ad..31da21a9f92 100644 --- a/apps/server/src/modules/lesson/service/nexboard.service.ts +++ b/apps/server/src/modules/lesson/service/nexboard.service.ts @@ -1,4 +1,4 @@ -import { FeathersServiceProvider } from '@shared/infra/feathers/feathers-service.provider'; +import { FeathersServiceProvider } from '@infra/feathers/feathers-service.provider'; import { LegacyLogger } from '@src/core/logger'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; diff --git a/apps/server/src/modules/management/console/board-management.console.spec.ts b/apps/server/src/modules/management/console/board-management.console.spec.ts index d6027bece93..4fe62db18a5 100644 --- a/apps/server/src/modules/management/console/board-management.console.spec.ts +++ b/apps/server/src/modules/management/console/board-management.console.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { ObjectId } from 'bson'; import { BoardManagementUc } from '../uc/board-management.uc'; import { BoardManagementConsole } from './board-management.console'; diff --git a/apps/server/src/modules/management/console/board-management.console.ts b/apps/server/src/modules/management/console/board-management.console.ts index f2762eccb17..83e5d4d7961 100644 --- a/apps/server/src/modules/management/console/board-management.console.ts +++ b/apps/server/src/modules/management/console/board-management.console.ts @@ -1,4 +1,4 @@ -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { ObjectId } from 'bson'; import { Command, Console } from 'nestjs-console'; import { BoardManagementUc } from '../uc/board-management.uc'; diff --git a/apps/server/src/modules/management/console/database-management.console.spec.ts b/apps/server/src/modules/management/console/database-management.console.spec.ts index f987bddff41..44517e19396 100644 --- a/apps/server/src/modules/management/console/database-management.console.spec.ts +++ b/apps/server/src/modules/management/console/database-management.console.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { DatabaseManagementUc } from '../uc/database-management.uc'; import { DatabaseManagementConsole } from './database-management.console'; diff --git a/apps/server/src/modules/management/console/database-management.console.ts b/apps/server/src/modules/management/console/database-management.console.ts index 29d98005bfe..780072aa837 100644 --- a/apps/server/src/modules/management/console/database-management.console.ts +++ b/apps/server/src/modules/management/console/database-management.console.ts @@ -1,4 +1,4 @@ -import { ConsoleWriterService } from '@shared/infra/console/console-writer/console-writer.service'; +import { ConsoleWriterService } from '@infra/console/console-writer/console-writer.service'; import { Command, Console } from 'nestjs-console'; import { DatabaseManagementUc } from '../uc/database-management.uc'; diff --git a/apps/server/src/modules/management/management-server.module.ts b/apps/server/src/modules/management/management-server.module.ts index b7481646c11..c24bf90cc09 100644 --- a/apps/server/src/modules/management/management-server.module.ts +++ b/apps/server/src/modules/management/management-server.module.ts @@ -2,8 +2,8 @@ import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { DynamicModule, Module, NotFoundException } from '@nestjs/common'; import { ALL_ENTITIES } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory-database/types'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { MongoDatabaseModuleOptions } from '@infra/database/mongo-memory-database/types'; import { DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { ManagementModule } from './management.module'; diff --git a/apps/server/src/modules/management/management.module.ts b/apps/server/src/modules/management/management.module.ts index fc4c8bc08d3..c1ed6aed227 100644 --- a/apps/server/src/modules/management/management.module.ts +++ b/apps/server/src/modules/management/management.module.ts @@ -1,11 +1,11 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { ConsoleWriterService } from '@shared/infra/console'; -import { DatabaseManagementModule, DatabaseManagementService } from '@shared/infra/database'; -import { EncryptionModule } from '@shared/infra/encryption'; -import { FileSystemModule } from '@shared/infra/file-system'; -import { KeycloakConfigurationModule } from '@shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module'; +import { ConsoleWriterService } from '@infra/console'; +import { DatabaseManagementModule, DatabaseManagementService } from '@infra/database'; +import { EncryptionModule } from '@infra/encryption'; +import { FileSystemModule } from '@infra/file-system'; +import { KeycloakConfigurationModule } from '@infra/identity-management/keycloak-configuration/keycloak-configuration.module'; import { createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; import { serverConfig } from '@modules/server'; diff --git a/apps/server/src/modules/management/uc/board-management.uc.ts b/apps/server/src/modules/management/uc/board-management.uc.ts index d57af94e6e9..8fa595690e8 100644 --- a/apps/server/src/modules/management/uc/board-management.uc.ts +++ b/apps/server/src/modules/management/uc/board-management.uc.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { BoardExternalReferenceType, BoardNode, Course, EntityId, InputFormat } from '@shared/domain'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { cardNodeFactory, columnBoardNodeFactory, diff --git a/apps/server/src/modules/management/uc/database-management.uc.spec.ts b/apps/server/src/modules/management/uc/database-management.uc.spec.ts index d74ecb0475e..0aa2c005a39 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.spec.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.spec.ts @@ -4,13 +4,9 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { StorageProviderEntity, SystemEntity } from '@shared/domain'; -import { DatabaseManagementService } from '@shared/infra/database'; -import { - DefaultEncryptionService, - LdapEncryptionService, - SymetricKeyEncryptionService, -} from '@shared/infra/encryption'; -import { FileSystemAdapter } from '@shared/infra/file-system'; +import { DatabaseManagementService } from '@infra/database'; +import { DefaultEncryptionService, LdapEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { FileSystemAdapter } from '@infra/file-system'; import { setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { ObjectId } from 'mongodb'; diff --git a/apps/server/src/modules/management/uc/database-management.uc.ts b/apps/server/src/modules/management/uc/database-management.uc.ts index 5a51d249de9..7b3d034c504 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.ts @@ -3,9 +3,9 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { StorageProviderEntity, SystemEntity } from '@shared/domain'; -import { DatabaseManagementService } from '@shared/infra/database'; -import { DefaultEncryptionService, IEncryptionService, LdapEncryptionService } from '@shared/infra/encryption'; -import { FileSystemAdapter } from '@shared/infra/file-system'; +import { DatabaseManagementService } from '@infra/database'; +import { DefaultEncryptionService, IEncryptionService, LdapEncryptionService } from '@infra/encryption'; +import { FileSystemAdapter } from '@infra/file-system'; import { LegacyLogger } from '@src/core/logger'; import { orderBy } from 'lodash'; import { BsonConverter } from '../converter/bson.converter'; diff --git a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts index 817d7257330..a85e71c526f 100644 --- a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts +++ b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts @@ -1,7 +1,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { ConsoleWriterModule } from '@shared/infra/console'; +import { ConsoleWriterModule } from '@infra/console'; import { createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '../authentication/authentication.module'; diff --git a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts index 83a3e3ac47b..bc993271066 100644 --- a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts +++ b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts @@ -20,7 +20,7 @@ import { ProviderConsentSessionResponse, ProviderLoginResponse, ProviderRedirectResponse, -} from '@shared/infra/oauth-provider/dto'; +} from '@infra/oauth-provider/dto'; import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; import { ICurrentUser } from '@modules/authentication'; import { OauthProviderUc } from '@modules/oauth-provider/uc/oauth-provider.uc'; diff --git a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts index 054cca37ffa..97b16e8e49f 100644 --- a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts +++ b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts @@ -1,14 +1,14 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; -// import should be @shared/infra/oauth-provider +// import should be @infra/oauth-provider import { ProviderConsentResponse, ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, ProviderConsentSessionResponse, -} from '@shared/infra/oauth-provider/dto'; +} from '@infra/oauth-provider/dto'; import { ApiTags } from '@nestjs/swagger'; import { OauthProviderLogoutFlowUc } from '../uc/oauth-provider.logout-flow.uc'; import { OauthProviderLoginFlowUc } from '../uc/oauth-provider.login-flow.uc'; diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.spec.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.spec.ts index 14db6718476..d34571dcbb7 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.spec.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.spec.ts @@ -1,4 +1,4 @@ -import { AcceptLoginRequestBody } from '@shared/infra/oauth-provider/dto'; +import { AcceptLoginRequestBody } from '@infra/oauth-provider/dto'; import { LoginRequestBody } from '../controller/dto'; import { OauthProviderRequestMapper } from './oauth-provider-request.mapper'; diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts index e0d4c4aaef4..aa8aa988408 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts @@ -1,4 +1,4 @@ -import { AcceptLoginRequestBody } from '@shared/infra/oauth-provider/dto'; +import { AcceptLoginRequestBody } from '@infra/oauth-provider/dto'; import { LoginRequestBody } from '@modules/oauth-provider/controller/dto'; export class OauthProviderRequestMapper { diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts index 13119635f75..f28ab378771 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts @@ -5,7 +5,7 @@ import { ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, -} from '@shared/infra/oauth-provider/dto'; +} from '@infra/oauth-provider/dto'; import { ConsentResponse, ConsentSessionResponse, diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts index 01038c23526..c97b86366b0 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts @@ -5,7 +5,7 @@ import { ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, -} from '@shared/infra/oauth-provider/dto'; +} from '@infra/oauth-provider/dto'; import { ConsentResponse, ConsentSessionResponse, diff --git a/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts b/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts index ccbd1566cda..bf131e22eee 100644 --- a/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts +++ b/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { OauthProviderServiceModule } from '@shared/infra/oauth-provider'; +import { OauthProviderServiceModule } from '@infra/oauth-provider'; import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '@modules/authorization'; import { PseudonymModule } from '@modules/pseudonym'; diff --git a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts index 4289644d29e..7483ad140e5 100644 --- a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts +++ b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { OauthProviderServiceModule } from '@shared/infra/oauth-provider'; +import { OauthProviderServiceModule } from '@infra/oauth-provider'; import { TeamsRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { LtiToolModule } from '@modules/lti-tool'; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts index 6ce203ab5b7..d2eb1636e53 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, User } from '@shared/domain'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { setupEntities, userFactory } from '@shared/testing'; import { AuthorizationService } from '@modules/authorization'; import { ICurrentUser } from '@modules/authentication'; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts index 3595f00679b..18fd23ae788 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { OauthProviderService } from '@shared/infra/oauth-provider/index'; +import { OauthProviderService } from '@infra/oauth-provider/index'; import { Permission, User } from '@shared/domain/index'; import { AuthorizationService } from '@modules/authorization'; -import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { ICurrentUser } from '@modules/authentication'; @Injectable() diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts index b397b048dd4..e56700477a8 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts @@ -1,12 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { OauthProviderService } from '@shared/infra/oauth-provider/index'; +import { OauthProviderService } from '@infra/oauth-provider'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; -import { - AcceptConsentRequestBody, - ProviderConsentResponse, - ProviderRedirectResponse, -} from '@shared/infra/oauth-provider/dto'; +import { AcceptConsentRequestBody, ProviderConsentResponse, ProviderRedirectResponse } from '@infra/oauth-provider/dto'; import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; import { ICurrentUser } from '@modules/authentication'; import { ForbiddenException } from '@nestjs/common'; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts index eb91d8132fe..126f68f1b80 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts @@ -3,12 +3,12 @@ import { ProviderConsentResponse, ProviderRedirectResponse, RejectRequestBody, -} from '@shared/infra/oauth-provider/dto'; +} from '@infra/oauth-provider/dto'; import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; import { ICurrentUser } from '@modules/authentication'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; +import { OauthProviderService } from '@infra/oauth-provider'; import { IdToken } from '@modules/oauth-provider/interface/id-token'; @Injectable() diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts index a9225031d04..1c160b021d7 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiToolDO, Permission, Pseudonym, UserDO } from '@shared/domain'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { ProviderLoginResponse, ProviderRedirectResponse } from '@shared/infra/oauth-provider/dto'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderLoginResponse, ProviderRedirectResponse } from '@infra/oauth-provider/dto'; import { externalToolFactory, ltiToolDOFactory, diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts index dade1cb3f07..cfe208c477c 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts @@ -1,12 +1,8 @@ import { Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { Permission, Pseudonym, User, UserDO } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { - AcceptLoginRequestBody, - ProviderLoginResponse, - ProviderRedirectResponse, -} from '@shared/infra/oauth-provider/dto'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { AcceptLoginRequestBody, ProviderLoginResponse, ProviderRedirectResponse } from '@infra/oauth-provider/dto'; import { AuthorizationService } from '@modules/authorization'; import { AcceptQuery, LoginRequestBody, OAuthRejectableBody } from '@modules/oauth-provider/controller/dto'; import { OauthProviderRequestMapper } from '@modules/oauth-provider/mapper/oauth-provider-request.mapper'; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts index 778112840b2..62171565cda 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { OauthProviderLogoutFlowUc } from '@modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; -import { OauthProviderService } from '@shared/infra/oauth-provider/index'; +import { OauthProviderService } from '@infra/oauth-provider/index'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; describe('OauthProviderUc', () => { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.ts index 68f1eb95bf9..30f45ba4188 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { ProviderRedirectResponse } from '@shared/infra/oauth-provider/dto'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderRedirectResponse } from '@infra/oauth-provider/dto'; @Injectable() export class OauthProviderLogoutFlowUc { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts index f1205db3d28..2faf242f0e5 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts @@ -1,8 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { OauthProviderUc } from '@modules/oauth-provider/uc/oauth-provider.uc'; -import { OauthProviderService } from '@shared/infra/oauth-provider/index'; +import { OauthProviderService } from '@infra/oauth-provider/index'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ProviderConsentSessionResponse } from '@shared/infra/oauth-provider/dto'; +import { ProviderConsentSessionResponse } from '@infra/oauth-provider/dto'; describe('OauthProviderUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.ts index 74bf0543d90..39ad1effd7f 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; +import { OauthProviderService } from '@infra/oauth-provider'; import { EntityId } from '@shared/domain'; -import { ProviderConsentSessionResponse } from '@shared/infra/oauth-provider/dto/'; +import { ProviderConsentSessionResponse } from '@infra/oauth-provider/dto/'; @Injectable() export class OauthProviderUc { diff --git a/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts b/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts index eaaf07f4500..a259c405cfb 100644 --- a/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts +++ b/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Account, EntityId, SchoolEntity, SystemEntity, User } from '@shared/domain'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { KeycloakAdministrationService } from '@shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service'; +import { KeycloakAdministrationService } from '@infra/identity-management/keycloak-administration/service/keycloak-administration.service'; import { accountFactory, cleanupCollections, diff --git a/apps/server/src/modules/oauth/oauth.module.ts b/apps/server/src/modules/oauth/oauth.module.ts index 273a099159b..ae0f0eda48d 100644 --- a/apps/server/src/modules/oauth/oauth.module.ts +++ b/apps/server/src/modules/oauth/oauth.module.ts @@ -1,7 +1,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { CacheWrapperModule } from '@shared/infra/cache'; -import { EncryptionModule } from '@shared/infra/encryption'; +import { CacheWrapperModule } from '@infra/cache'; +import { EncryptionModule } from '@infra/encryption'; import { LtiToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '@modules/authorization'; diff --git a/apps/server/src/modules/oauth/service/hydra.service.spec.ts b/apps/server/src/modules/oauth/service/hydra.service.spec.ts index 3886aa40a58..2dc2a22a6ce 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.spec.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.spec.ts @@ -6,7 +6,7 @@ import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiPrivacyPermission, LtiRoleType, OauthConfig } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { LtiToolRepo } from '@shared/repo'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/modules/oauth/service/hydra.service.ts b/apps/server/src/modules/oauth/service/hydra.service.ts index 9b335604825..360926d080a 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.ts @@ -4,7 +4,7 @@ import { Inject, InternalServerErrorException } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { OauthConfig } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { LtiToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationParams } from '@modules/oauth/controller/dto/authorization.params'; diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 2743037e214..9c4b45582df 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { ProvisioningDto, ProvisioningService } from '@modules/provisioning'; diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 28a24c0534a..190c962cd97 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -2,7 +2,7 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { EntityId, LegacySchoolDo, OauthConfig, SchoolFeatures, UserDO } from '@shared/domain'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; +import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { LegacyLogger } from '@src/core/logger'; import { ProvisioningService } from '@modules/provisioning'; import { OauthDataDto } from '@modules/provisioning/dto'; diff --git a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts index 9d3711aff02..324e444afcb 100644 --- a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts +++ b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Page, Pseudonym } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, externalToolPseudonymEntityFactory, pseudonymFactory, userFactory } from '@shared/testing'; import { pseudonymEntityFactory } from '@shared/testing/factory/pseudonym.factory'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts index 548ba1b0512..8246479da4a 100644 --- a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts +++ b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Pseudonym } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, pseudonymFactory, userFactory } from '@shared/testing'; import { pseudonymEntityFactory } from '@shared/testing/factory/pseudonym.factory'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 09dd2210928..becc7ec78fc 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -1,5 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import type { IIdentityManagementConfig } from '@shared/infra/identity-management'; +import type { IIdentityManagementConfig } from '@infra/identity-management'; import type { ICoreModuleConfig } from '@src/core'; import type { IAccountConfig } from '@modules/account'; import type { IFilesStorageClientConfig } from '@modules/files-storage-client'; diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index 9454fa06154..a812ff773b2 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -28,10 +28,10 @@ import { VideoConferenceApiModule } from '@modules/video-conference/video-confer import { DynamicModule, Inject, MiddlewareConsumer, Module, NestModule, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain'; -import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { MailModule } from '@shared/infra/mail'; -import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; -import { RedisModule, REDIS_CLIENT } from '@shared/infra/redis'; +import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; +import { MailModule } from '@infra/mail'; +import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@infra/rabbitmq'; +import { RedisModule, REDIS_CLIENT } from '@infra/redis'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LegacyLogger, LoggerModule } from '@src/core/logger'; diff --git a/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts b/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts index 4bd73e6c2a8..f5e01b0e9ea 100644 --- a/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts +++ b/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts @@ -1,7 +1,7 @@ import { createMock } from '@golevelup/ts-jest'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, schoolFactory, shareTokenFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { ShareTokenContextType } from '../domainobject/share-token.do'; diff --git a/apps/server/src/modules/system/service/system.service.spec.ts b/apps/server/src/modules/system/service/system.service.spec.ts index ead44bf6133..89ef533058b 100644 --- a/apps/server/src/modules/system/service/system.service.spec.ts +++ b/apps/server/src/modules/system/service/system.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { OauthConfig, SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { IdentityManagementOauthService } from '@shared/infra/identity-management'; +import { IdentityManagementOauthService } from '@infra/identity-management'; import { SystemRepo } from '@shared/repo'; import { systemFactory } from '@shared/testing'; import { SystemMapper } from '../mapper/system.mapper'; diff --git a/apps/server/src/modules/system/service/system.service.ts b/apps/server/src/modules/system/service/system.service.ts index 960c15f7945..bfb6a2ec7bf 100644 --- a/apps/server/src/modules/system/service/system.service.ts +++ b/apps/server/src/modules/system/service/system.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { IdentityManagementOauthService } from '@shared/infra/identity-management/identity-management-oauth.service'; +import { IdentityManagementOauthService } from '@infra/identity-management/identity-management-oauth.service'; import { SystemRepo } from '@shared/repo'; import { SystemMapper } from '@modules/system/mapper/system.mapper'; import { SystemDto } from '@modules/system/service/dto/system.dto'; diff --git a/apps/server/src/modules/system/system.module.ts b/apps/server/src/modules/system/system.module.ts index 64caef0df61..37ca8d7a858 100644 --- a/apps/server/src/modules/system/system.module.ts +++ b/apps/server/src/modules/system/system.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { IdentityManagementModule } from '@shared/infra/identity-management/identity-management.module'; +import { IdentityManagementModule } from '@infra/identity-management/identity-management.module'; import { SystemRepo } from '@shared/repo'; import { SystemService } from '@modules/system/service/system.service'; import { SystemOidcService } from './service/system-oidc.service'; diff --git a/apps/server/src/modules/task/service/submission.service.spec.ts b/apps/server/src/modules/task/service/submission.service.spec.ts index 4d7373570cf..abf5ed9f152 100644 --- a/apps/server/src/modules/task/service/submission.service.spec.ts +++ b/apps/server/src/modules/task/service/submission.service.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { Counted, Submission } from '@shared/domain'; -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { SubmissionRepo } from '@shared/repo'; import { setupEntities, submissionFactory, taskFactory } from '@shared/testing'; import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; diff --git a/apps/server/src/modules/tool/external-tool/external-tool.module.ts b/apps/server/src/modules/tool/external-tool/external-tool.module.ts index 7db5c25a252..2fbd2f28edd 100644 --- a/apps/server/src/modules/tool/external-tool/external-tool.module.ts +++ b/apps/server/src/modules/tool/external-tool/external-tool.module.ts @@ -1,8 +1,8 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { OauthProviderServiceModule } from '@shared/infra/oauth-provider'; -import { EncryptionModule } from '@shared/infra/encryption'; +import { OauthProviderServiceModule } from '@infra/oauth-provider'; +import { EncryptionModule } from '@infra/encryption'; import { ExternalToolRepo } from '@shared/repo'; import { ToolConfigModule } from '../tool-config.module'; import { diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.spec.ts index 53a07c02e30..fb506a22f8c 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { ExternalToolServiceMapper } from './external-tool-service.mapper'; import { TokenEndpointAuthMethod, ToolConfigType } from '../../common/enum'; import { Oauth2ToolConfig } from '../domain'; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.ts index 31ff93db828..c531f33c483 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-service.mapper.ts @@ -1,4 +1,4 @@ -import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { Injectable } from '@nestjs/common'; import { Oauth2ToolConfig } from '../domain'; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts index 4db2a5be0b0..ddb88ca4ff3 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts @@ -2,9 +2,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { IFindOptions, Page, SortOrder } from '@shared/domain'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; +import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { externalToolFactory, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index fcc1a7e2d5c..0aa6181682c 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; import { EntityId, IFindOptions, Page } from '@shared/domain'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; -import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; +import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { TokenEndpointAuthMethod } from '../../common/enum'; diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts index 3c3baa56475..ff4aa4c266c 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts @@ -16,7 +16,7 @@ import { SystemEntity, User, } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; import { federalStateFactory, importUserFactory, schoolFactory, userFactory } from '@shared/testing'; import { systemFactory } from '@shared/testing/factory/system.factory'; diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts index c7a1ed30668..28711a06c1a 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts @@ -12,7 +12,7 @@ import { VideoConferenceDO, VideoConferenceScope, } from '@shared/domain'; -import { CalendarEventDto, CalendarService } from '@shared/infra/calendar'; +import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.ts b/apps/server/src/modules/video-conference/service/video-conference.service.ts index 22e7a7462f1..401cc0a0015 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.ts @@ -14,7 +14,7 @@ import { VideoConferenceOptionsDO, VideoConferenceScope, } from '@shared/domain'; -import { CalendarEventDto, CalendarService } from '@shared/infra/calendar'; +import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { CourseService } from '@modules/learnroom'; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts index 994c8042a6d..bf1fd3e1394 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts @@ -14,8 +14,8 @@ import { VideoConferenceDO, } from '@shared/domain'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { CalendarService } from '@shared/infra/calendar'; -import { CalendarEventDto } from '@shared/infra/calendar/dto/calendar-event.dto'; +import { CalendarService } from '@infra/calendar'; +import { CalendarEventDto } from '@infra/calendar/dto/calendar-event.dto'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { roleFactory, setupEntities, userDoFactory } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts index 41e011c4acd..95470053e6d 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts @@ -13,8 +13,8 @@ import { VideoConferenceOptionsDO, } from '@shared/domain'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { CalendarService } from '@shared/infra/calendar'; -import { CalendarEventDto } from '@shared/infra/calendar/dto/calendar-event.dto'; +import { CalendarService } from '@infra/calendar'; +import { CalendarEventDto } from '@infra/calendar/dto/calendar-event.dto'; import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; import { ICurrentUser } from '@modules/authentication'; diff --git a/apps/server/src/modules/video-conference/video-conference.module.ts b/apps/server/src/modules/video-conference/video-conference.module.ts index c9708b16dc9..d7e4671c0f2 100644 --- a/apps/server/src/modules/video-conference/video-conference.module.ts +++ b/apps/server/src/modules/video-conference/video-conference.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; -import { CalendarModule } from '@shared/infra/calendar'; +import { CalendarModule } from '@infra/calendar'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; import { AuthorizationModule } from '@modules/authorization'; import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; diff --git a/apps/server/src/shared/domain/entity/all-entities.spec.ts b/apps/server/src/shared/domain/entity/all-entities.spec.ts index 33d924b20ce..b7a27fd58b4 100644 --- a/apps/server/src/shared/domain/entity/all-entities.spec.ts +++ b/apps/server/src/shared/domain/entity/all-entities.spec.ts @@ -1,7 +1,7 @@ import { MikroORM } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ALL_ENTITIES } from '.'; describe('BaseRepo', () => { diff --git a/apps/server/src/shared/infra/collaborative-storage/index.ts b/apps/server/src/shared/infra/collaborative-storage/index.ts deleted file mode 100644 index ea5aa25514c..00000000000 --- a/apps/server/src/shared/infra/collaborative-storage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './collaborative-storage.adapter'; diff --git a/apps/server/src/shared/repo/base.do.repo.integration.spec.ts b/apps/server/src/shared/repo/base.do.repo.integration.spec.ts index 563940a571e..1ca91bd184e 100644 --- a/apps/server/src/shared/repo/base.do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/base.do.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Entity, EntityName, Property } from '@mikro-orm/core'; import { BaseDO, BaseEntityWithTimestamps } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { Injectable } from '@nestjs/common'; import { BaseDORepo } from '@shared/repo/base.do.repo'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/shared/repo/base.repo.integration.spec.ts b/apps/server/src/shared/repo/base.repo.integration.spec.ts index 0c0e041f575..372a6ba4cda 100644 --- a/apps/server/src/shared/repo/base.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/base.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Entity, EntityName, Property } from '@mikro-orm/core'; import { BaseEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { Injectable } from '@nestjs/common'; import { BaseRepo } from './base.repo'; diff --git a/apps/server/src/shared/repo/board/board.repo.spec.ts b/apps/server/src/shared/repo/board/board.repo.spec.ts index 51ce325e90a..7d5f4b71e41 100644 --- a/apps/server/src/shared/repo/board/board.repo.spec.ts +++ b/apps/server/src/shared/repo/board/board.repo.spec.ts @@ -10,7 +10,7 @@ import { cleanupCollections, } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { BoardRepo } from './board.repo'; diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts index 0a9151d8c9c..854c3958135 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ExternalToolRepoMapper } from '@shared/repo/externaltool/external-tool.repo.mapper'; import { cleanupCollections, diff --git a/apps/server/src/shared/repo/course/course.repo.integration.spec.ts b/apps/server/src/shared/repo/course/course.repo.integration.spec.ts index 5474c4ec19d..2a77ad1f078 100644 --- a/apps/server/src/shared/repo/course/course.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/course/course.repo.integration.spec.ts @@ -3,7 +3,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Course, EntityId, SortOrder } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, courseFactory, courseGroupFactory, userFactory } from '@shared/testing'; import { CourseRepo } from './course.repo'; diff --git a/apps/server/src/shared/repo/coursegroup/coursegroup.repo.integration.spec.ts b/apps/server/src/shared/repo/coursegroup/coursegroup.repo.integration.spec.ts index 459a2add113..805088480fd 100644 --- a/apps/server/src/shared/repo/coursegroup/coursegroup.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/coursegroup/coursegroup.repo.integration.spec.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityId, CourseGroup, Course } from '@shared/domain'; import { courseFactory, courseGroupFactory } from '@shared/testing'; import { CourseGroupRepo } from './coursegroup.repo'; diff --git a/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.spec.ts b/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.spec.ts index 4f242caecd8..32b35ced07e 100644 --- a/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.spec.ts +++ b/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.spec.ts @@ -1,6 +1,6 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { InternalServerErrorException } from '@nestjs/common'; import { DashboardEntity, 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 1b3e2aefd63..3ca95112f4d 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 @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { DashboardEntity, DashboardGridElementModel, GridElement } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { courseFactory, userFactory } from '@shared/testing'; import { DashboardModelMapper } from './dashboard.model.mapper'; import { DashboardRepo } from './dashboard.repo'; diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts index 56cba80ec69..1654da1b5b7 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { IFindOptions, Page, SortOrder } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ExternalToolRepo, ExternalToolRepoMapper } from '@shared/repo'; import { cleanupCollections, externalToolEntityFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/shared/repo/federalstate/federal-state.repo.spec.ts b/apps/server/src/shared/repo/federalstate/federal-state.repo.spec.ts index bfd6b6e358e..0b5742c7621 100644 --- a/apps/server/src/shared/repo/federalstate/federal-state.repo.spec.ts +++ b/apps/server/src/shared/repo/federalstate/federal-state.repo.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EntityManager } from '@mikro-orm/mongodb'; import { FederalStateEntity } from '@shared/domain'; import { cleanupCollections, federalStateFactory } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { FederalStateRepo } from './federal-state.repo'; describe('FederalStateRepo', () => { diff --git a/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts b/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts index 7f02bf4ea01..148984c393a 100644 --- a/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts @@ -12,7 +12,7 @@ import { SchoolEntity, User, } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ImportUserRepo } from '.'; describe('ImportUserRepo', () => { diff --git a/apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts b/apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts index 5143eb6fba8..eae071d55ae 100644 --- a/apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ComponentType, IComponentProperties, LessonEntity } from '@shared/domain'; import { cleanupCollections, courseFactory, lessonFactory, materialFactory, taskFactory } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { LessonRepo } from './lesson.repo'; diff --git a/apps/server/src/shared/repo/ltitool/ltitool.repo.spec.ts b/apps/server/src/shared/repo/ltitool/ltitool.repo.spec.ts index c8b8a7aea50..97fc81e9ea1 100644 --- a/apps/server/src/shared/repo/ltitool/ltitool.repo.spec.ts +++ b/apps/server/src/shared/repo/ltitool/ltitool.repo.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ILtiToolProperties, LtiTool } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { LtiPrivacyPermission, LtiRoleType } from '@shared/domain/entity/ltitool.entity'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { LtiToolRepo } from '@shared/repo/ltitool/ltitool.repo'; import { cleanupCollections } from '@shared/testing'; import { ltiToolFactory } from '@shared/testing/factory/ltitool.factory'; diff --git a/apps/server/src/shared/repo/materials/materials.repo.integration.spec.ts b/apps/server/src/shared/repo/materials/materials.repo.integration.spec.ts index 13556fad62c..0b6b26ff39f 100644 --- a/apps/server/src/shared/repo/materials/materials.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/materials/materials.repo.integration.spec.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Material } from '@shared/domain/entity/materials.entity'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections } from '@shared/testing'; import { MaterialsRepo } from './materials.repo'; diff --git a/apps/server/src/shared/repo/news/news-inheritance.spec.ts b/apps/server/src/shared/repo/news/news-inheritance.spec.ts index 53eb2aa4dd4..d9d5a003d5d 100644 --- a/apps/server/src/shared/repo/news/news-inheritance.spec.ts +++ b/apps/server/src/shared/repo/news/news-inheritance.spec.ts @@ -2,7 +2,7 @@ import { Collection, Entity, Enum, ManyToMany, ManyToOne, Property } from '@mikr import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { BaseEntityWithTimestamps } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections } from '@shared/testing'; @Entity({ tableName: 'users' }) diff --git a/apps/server/src/shared/repo/news/news.repo.integration.spec.ts b/apps/server/src/shared/repo/news/news.repo.integration.spec.ts index 93bff583f4e..d68e50e0df1 100644 --- a/apps/server/src/shared/repo/news/news.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/news/news.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { News, NewsTargetModel, SortOrder } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { courseNewsFactory, schoolNewsFactory, diff --git a/apps/server/src/shared/repo/role/role.repo.integration.spec.ts b/apps/server/src/shared/repo/role/role.repo.integration.spec.ts index 04834698419..65bc0931a6e 100644 --- a/apps/server/src/shared/repo/role/role.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/role/role.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { NotFoundError, NullCacheAdapter, ValidationError } from '@mikro-orm/cor import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Role, RoleName } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, roleFactory } from '@shared/testing'; import { RoleRepo } from './role.repo'; diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts index e9f14ba3315..775c193675d 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts @@ -13,7 +13,7 @@ import { SystemEntity, UserLoginMigrationEntity, } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { legacySchoolDoFactory, schoolFactory, diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts index a2844e8e426..47196013b63 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { type SchoolEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ExternalToolRepoMapper } from '@shared/repo/externaltool/external-tool.repo.mapper'; import { cleanupCollections, diff --git a/apps/server/src/shared/repo/storageprovider/storageprovider.repo.spec.ts b/apps/server/src/shared/repo/storageprovider/storageprovider.repo.spec.ts index 707ff1b778d..1f24fe0bd0b 100644 --- a/apps/server/src/shared/repo/storageprovider/storageprovider.repo.spec.ts +++ b/apps/server/src/shared/repo/storageprovider/storageprovider.repo.spec.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { StorageProviderEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, storageProviderFactory } from '@shared/testing'; import { StorageProviderRepo } from './storageprovider.repo'; diff --git a/apps/server/src/shared/repo/submission/submission.repo.integration.spec.ts b/apps/server/src/shared/repo/submission/submission.repo.integration.spec.ts index 772c9d4ea1b..547fe2f05a1 100644 --- a/apps/server/src/shared/repo/submission/submission.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/submission/submission.repo.integration.spec.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Submission } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, courseFactory, diff --git a/apps/server/src/shared/repo/system/system.repo.integration.spec.ts b/apps/server/src/shared/repo/system/system.repo.integration.spec.ts index 7c3c10a2fab..d8a5c1d87e3 100644 --- a/apps/server/src/shared/repo/system/system.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/system/system.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { SystemRepo } from '@shared/repo'; import { systemFactory } from '@shared/testing/factory/system.factory'; diff --git a/apps/server/src/shared/repo/task/task.repo.integration.spec.ts b/apps/server/src/shared/repo/task/task.repo.integration.spec.ts index 52f20ab2cab..b975f485dea 100644 --- a/apps/server/src/shared/repo/task/task.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/task/task.repo.integration.spec.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SortOrder, Task } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, courseFactory, diff --git a/apps/server/src/shared/repo/teams/team.repo.integration.spec.ts b/apps/server/src/shared/repo/teams/team.repo.integration.spec.ts index 8d68ea8cf21..92e3d9d63c7 100644 --- a/apps/server/src/shared/repo/teams/team.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/teams/team.repo.integration.spec.ts @@ -2,7 +2,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, TeamEntity, TeamUserEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { TeamsRepo } from '@shared/repo'; import { cleanupCollections, roleFactory } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; diff --git a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts index ddcfb55b520..43d46a95383 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts @@ -16,7 +16,7 @@ import { } from '@shared/domain'; import { Page } from '@shared/domain/domainobject/page'; import { UserDO } from '@shared/domain/domainobject/user.do'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { cleanupCollections, 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 d469b59c14b..ea10e6e5b3e 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 @@ -2,7 +2,7 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { MatchCreator, SortOrder, SystemEntity, User } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, importUserFactory, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { systemFactory } from '@shared/testing/factory/system.factory'; import { UserRepo } from './user.repo'; diff --git a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts index 230e715307a..266cd0381c1 100644 --- a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts @@ -3,7 +3,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity, SystemEntity, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, schoolFactory, systemFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { userLoginMigrationFactory } from '../../testing/factory/user-login-migration.factory'; diff --git a/apps/server/src/shared/repo/videoconference/video-conference.repo.spec.ts b/apps/server/src/shared/repo/videoconference/video-conference.repo.spec.ts index 239b80fc3e9..06e4f057fa7 100644 --- a/apps/server/src/shared/repo/videoconference/video-conference.repo.spec.ts +++ b/apps/server/src/shared/repo/videoconference/video-conference.repo.spec.ts @@ -1,7 +1,7 @@ import { VideoConferenceRepo } from '@shared/repo'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections } from '@shared/testing'; import { IVideoConferenceProperties, diff --git a/apps/server/src/shared/testing/factory/filerecord.factory.ts b/apps/server/src/shared/testing/factory/filerecord.factory.ts index 36811ed9752..4e12787f661 100644 --- a/apps/server/src/shared/testing/factory/filerecord.factory.ts +++ b/apps/server/src/shared/testing/factory/filerecord.factory.ts @@ -1,4 +1,4 @@ -import { FileRecordParentType } from '@shared/infra/rabbitmq'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { FileRecord, FileRecordSecurityCheck, IFileRecordProperties } from '@modules/files-storage/entity'; import { ObjectId } from 'bson'; import { DeepPartial } from 'fishery'; diff --git a/jest.config.ts b/jest.config.ts index 032f4828dde..9ee33256a5e 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -26,6 +26,7 @@ let config: Config.InitialOptions = { '^@shared/(.*)$': '/apps/server/src/shared/$1', '^@src/(.*)$': '/apps/server/src/$1', '^@modules/(.*)$': '/apps/server/src/modules/$1', + '^@infra/(.*)$': '/apps/server/src/infra/$1', }, maxWorkers: 2, // limited for not taking all workers within of a single github action }; diff --git a/tsconfig.json b/tsconfig.json index 9bb7c6f72f0..147df972b6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "@shared/*": ["apps/server/src/shared/*"], "@src/*": ["apps/server/src/*"], "@modules/*": ["apps/server/src/modules/*"], + "@infra/*": ["apps/server/src/infra/*"], }, } } From e82f2d768ef2fa541205cb5cd25ee27804a0c68f Mon Sep 17 00:00:00 2001 From: WojciechGrancow <116577704+WojciechGrancow@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:27:31 +0100 Subject: [PATCH 06/23] BC-5695-RocketChat-user-deletion (#4523) * first commit * add some tests * add test cases and services * add usecases and test cases * fix importing * add type in uc * fix import * implement entity, domain, mapper and repo for rocketChat * fix most of issue form review * add some test,additional status, limit ... * implement rocketChat User deletion * add service to module * small fixes during review * implement deletion rocketChat user in KNL deletion module * small fixes * changing limit parameter and changing in useCases * delete 3 lines of code * Update apps/server/src/modules/deletion/uc/deletion-request.uc.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * Update apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * small fixes * Update apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> * import fix --------- Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> --- .../types/deletion-domain-model.enum.ts | 1 + .../deletion/uc/deletion-request.uc.spec.ts | 54 +++++++- .../deletion/uc/deletion-request.uc.ts | 18 ++- .../modules/rocketchat-user/domain/index.ts | 1 + .../domain/rocket-chat-user.do.spec.ts | 67 +++++++++ .../domain/rocket-chat-user.do.ts | 37 +++++ .../rocketchat-user/domain/testing/index.ts | 1 + .../testing/rocket-chat-user.factory.ts | 18 +++ .../modules/rocketchat-user/entity/index.ts | 1 + .../entity/rocket-chat-user.entity.spec.ts | 61 ++++++++ .../entity/rocket-chat-user.entity.ts | 56 ++++++++ .../rocketchat-user/entity/testing/index.ts | 1 + .../rocket-chat-user.entity.factory.ts | 20 +++ .../src/modules/rocketchat-user/index.ts | 3 + .../src/modules/rocketchat-user/repo/index.ts | 1 + .../rocketchat-user/repo/mapper/index.ts | 1 + .../mapper/rocket-chat-user.mapper.spec.ts | 61 ++++++++ .../repo/mapper/rocket-chat-user.mapper.ts | 29 ++++ .../repo/rocket-chat-user.repo.spec.ts | 131 ++++++++++++++++++ .../repo/rocket-chat-user.repo.ts | 33 +++++ .../rocketchat-user/rocketchat-user.module.ts | 10 ++ .../modules/rocketchat-user/service/index.ts | 1 + .../service/rocket-chat-user.service.spec.ts | 94 +++++++++++++ .../service/rocket-chat-user.service.ts | 19 +++ 24 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/modules/rocketchat-user/domain/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.spec.ts create mode 100644 apps/server/src/modules/rocketchat-user/domain/rocket-chat-user.do.ts create mode 100644 apps/server/src/modules/rocketchat-user/domain/testing/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/domain/testing/rocket-chat-user.factory.ts create mode 100644 apps/server/src/modules/rocketchat-user/entity/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.spec.ts create mode 100644 apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts create mode 100644 apps/server/src/modules/rocketchat-user/entity/testing/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/entity/testing/rocket-chat-user.entity.factory.ts create mode 100644 apps/server/src/modules/rocketchat-user/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/repo/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/repo/mapper/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.spec.ts create mode 100644 apps/server/src/modules/rocketchat-user/repo/mapper/rocket-chat-user.mapper.ts create mode 100644 apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.spec.ts create mode 100644 apps/server/src/modules/rocketchat-user/repo/rocket-chat-user.repo.ts create mode 100644 apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts create mode 100644 apps/server/src/modules/rocketchat-user/service/index.ts create mode 100644 apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts create mode 100644 apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts 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); + } +} From 33381e6834014743f33866d1c02def649930d466 Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Wed, 8 Nov 2023 14:09:48 +0100 Subject: [PATCH 07/23] N21-1398 hide mail section for ldap (#4535) * makes ldap to external system for skipping email section on first login --- .../controllers/api-test/login.api.spec.ts | 3 +- .../modules/authentication/interface/index.ts | 1 + .../interface/oauth-current-user.ts | 6 ++ .../modules/authentication/interface/user.ts | 7 +- .../mapper/current-user.mapper.spec.ts | 96 +++++++++++++------ .../mapper/current-user.mapper.ts | 4 +- .../strategy/ldap.strategy.spec.ts | 4 +- .../authentication/strategy/ldap.strategy.ts | 4 +- .../authentication/strategy/local.strategy.ts | 2 +- 9 files changed, 83 insertions(+), 44 deletions(-) create mode 100644 apps/server/src/modules/authentication/interface/oauth-current-user.ts diff --git a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts index 7da3c21eab9..04683e182a8 100644 --- a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts +++ b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts @@ -192,6 +192,7 @@ describe('Login Controller (api)', () => { expect(decodedToken).toHaveProperty('accountId'); expect(decodedToken).toHaveProperty('schoolId'); expect(decodedToken).toHaveProperty('roles'); + expect(decodedToken).toHaveProperty('isExternalUser'); expect(decodedToken).not.toHaveProperty('externalIdToken'); }); }); @@ -287,7 +288,7 @@ describe('Login Controller (api)', () => { roles: [studentRole.id], schoolId: school.id, accountId: account.id, - isExternalUser: false, + isExternalUser: true, }); expect(decodedToken).not.toHaveProperty('externalIdToken'); }); diff --git a/apps/server/src/modules/authentication/interface/index.ts b/apps/server/src/modules/authentication/interface/index.ts index e5abc856509..a9de8109a5b 100644 --- a/apps/server/src/modules/authentication/interface/index.ts +++ b/apps/server/src/modules/authentication/interface/index.ts @@ -1 +1,2 @@ export * from './user'; +export * from './oauth-current-user'; diff --git a/apps/server/src/modules/authentication/interface/oauth-current-user.ts b/apps/server/src/modules/authentication/interface/oauth-current-user.ts new file mode 100644 index 00000000000..ddf15e1ca5d --- /dev/null +++ b/apps/server/src/modules/authentication/interface/oauth-current-user.ts @@ -0,0 +1,6 @@ +import { ICurrentUser } from './user'; + +export interface OauthCurrentUser extends ICurrentUser { + /** Contains the idToken of the external idp. Will be set during oAuth2 login and used for rp initiated logout */ + externalIdToken?: string; +} diff --git a/apps/server/src/modules/authentication/interface/user.ts b/apps/server/src/modules/authentication/interface/user.ts index cc8423f69b7..82b6d292d50 100644 --- a/apps/server/src/modules/authentication/interface/user.ts +++ b/apps/server/src/modules/authentication/interface/user.ts @@ -16,11 +16,6 @@ export interface ICurrentUser { /** True if a support member impersonates the user */ impersonated?: boolean; - /** True if the user is an external user e.g. an oauth user */ + /** True if the user is an external user e.g. an oauth user or ldap user */ isExternalUser: boolean; } - -export interface OauthCurrentUser extends ICurrentUser { - /** Contains the idToken of the external idp. Will be set during oAuth2 login and used for rp initiated logout */ - externalIdToken?: string; -} diff --git a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts index d06bea6d080..104ca1219a4 100644 --- a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts +++ b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts @@ -15,42 +15,78 @@ describe('CurrentUserMapper', () => { describe('userToICurrentUser', () => { describe('when mapping from a user entity to the current user object', () => { - it('should map with roles', () => { - const teacherRole = roleFactory.buildWithId({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); - const user = userFactory.buildWithId({ - roles: [teacherRole], - }); - const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(accountId, user); - expect(currentUser).toMatchObject({ - accountId, - systemId: undefined, - roles: [teacherRole.id], - schoolId: null, + describe('when user has roles', () => { + const setup = () => { + const teacherRole = roleFactory.buildWithId({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT], + }); + const user = userFactory.buildWithId({ + roles: [teacherRole], + }); + + return { + teacherRole, + user, + }; + }; + + it('should map with roles', () => { + const { teacherRole, user } = setup(); + + const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(accountId, user, false); + + expect(currentUser).toMatchObject({ + accountId, + systemId: undefined, + roles: [teacherRole.id], + schoolId: null, + isExternalUser: false, + }); }); }); - it('should map without roles', () => { - const user = userFactory.buildWithId(); - const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(accountId, user); - expect(currentUser).toMatchObject({ - accountId, - systemId: undefined, - roles: [], - schoolId: null, + describe('when user has no roles', () => { + it('should map without roles', () => { + const user = userFactory.buildWithId(); + + const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(accountId, user, true); + + expect(currentUser).toMatchObject({ + accountId, + systemId: undefined, + roles: [], + schoolId: null, + isExternalUser: true, + }); }); }); - it('should map system and school', () => { - const user = userFactory.buildWithId({ - school: schoolFactory.buildWithId(), - }); - const systemId = 'mockSystemId'; - const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(accountId, user, systemId); - expect(currentUser).toMatchObject({ - accountId, - systemId, - roles: [], - schoolId: user.school.id, + describe('when systemId is provided', () => { + const setup = () => { + const user = userFactory.buildWithId({ + school: schoolFactory.buildWithId(), + }); + const systemId = 'mockSystemId'; + + return { + user, + systemId, + }; + }; + + it('should map system and school', () => { + const { user, systemId } = setup(); + + const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(accountId, user, false, systemId); + + expect(currentUser).toMatchObject({ + accountId, + systemId, + roles: [], + schoolId: user.school.id, + isExternalUser: false, + }); }); }); }); diff --git a/apps/server/src/modules/authentication/mapper/current-user.mapper.ts b/apps/server/src/modules/authentication/mapper/current-user.mapper.ts index ab832b70d8c..35dd6c5fe7c 100644 --- a/apps/server/src/modules/authentication/mapper/current-user.mapper.ts +++ b/apps/server/src/modules/authentication/mapper/current-user.mapper.ts @@ -6,14 +6,14 @@ import { ICurrentUser, OauthCurrentUser } from '../interface'; import { CreateJwtPayload, JwtPayload } from '../interface/jwt-payload'; export class CurrentUserMapper { - static userToICurrentUser(accountId: string, user: User, systemId?: string): ICurrentUser { + static userToICurrentUser(accountId: string, user: User, isExternalUser: boolean, systemId?: string): ICurrentUser { return { accountId, systemId, roles: user.roles.getItems().map((role: Role) => role.id), schoolId: user.school.id, userId: user.id, - isExternalUser: false, + isExternalUser, }; } diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts index 78f445ce5b0..5b23be9eb74 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -436,7 +436,7 @@ describe('LdapStrategy', () => { schoolId: school.id, systemId: system.id, accountId: account.id, - isExternalUser: false, + isExternalUser: true, }); }); }); @@ -501,7 +501,7 @@ describe('LdapStrategy', () => { schoolId: school.id, systemId: system.id, accountId: account.id, - isExternalUser: false, + isExternalUser: true, }); }); }); diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts index 1622e434310..6f33e92f21a 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts @@ -1,10 +1,10 @@ +import { AccountDto } from '@modules/account/services/dto'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { LegacySchoolDo, SystemEntity, User } from '@shared/domain'; import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { ErrorLoggable } from '@src/core/error/loggable/error.loggable'; import { Logger } from '@src/core/logger'; -import { AccountDto } from '@modules/account/services/dto'; import { Strategy } from 'passport-custom'; import { LdapAuthorizationBodyParams } from '../controllers/dto'; import { ICurrentUser } from '../interface'; @@ -48,7 +48,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { await this.checkCredentials(account, system, ldapDn, password); - const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(account.id, user, systemId); + const currentUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(account.id, user, true, systemId); return currentUser; } diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.ts b/apps/server/src/modules/authentication/strategy/local.strategy.ts index 28ed573b45e..1d31a86d833 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.ts @@ -39,7 +39,7 @@ export class LocalStrategy extends PassportStrategy(Strategy) { new Error(`login failing, because account ${account.id} has no userId`) ); const user = await this.userRepo.findById(accountUserId, true); - const currentUser = CurrentUserMapper.userToICurrentUser(account.id, user); + const currentUser = CurrentUserMapper.userToICurrentUser(account.id, user, false); return currentUser; } From cf3ce9ceb3181ecb6dcd4f04f06689f487410b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:16:17 +0100 Subject: [PATCH 08/23] N21-726 Add location to school name during provisioning (#4532) --- .../provisioning/dto/external-school.dto.ts | 3 + .../service/oidc-provisioning.service.spec.ts | 475 +++++++++++++++--- .../oidc/service/oidc-provisioning.service.ts | 23 +- .../strategy/sanis/response/index.ts | 1 + .../response/sanis-anschrift-response.ts | 9 + .../response/sanis-organisation-response.ts | 4 + .../sanis/sanis-response.mapper.spec.ts | 6 +- .../strategy/sanis/sanis-response.mapper.ts | 3 +- 8 files changed, 434 insertions(+), 90 deletions(-) create mode 100644 apps/server/src/modules/provisioning/strategy/sanis/response/sanis-anschrift-response.ts diff --git a/apps/server/src/modules/provisioning/dto/external-school.dto.ts b/apps/server/src/modules/provisioning/dto/external-school.dto.ts index c853c090228..701ee63f931 100644 --- a/apps/server/src/modules/provisioning/dto/external-school.dto.ts +++ b/apps/server/src/modules/provisioning/dto/external-school.dto.ts @@ -5,9 +5,12 @@ export class ExternalSchoolDto { officialSchoolNumber?: string; + location?: string; + constructor(props: ExternalSchoolDto) { this.externalId = props.externalId; this.name = props.name; this.officialSchoolNumber = props.officialSchoolNumber; + this.location = props.location; } } diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts index 09e253dddbf..5fcd5fc37a5 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts @@ -1,28 +1,29 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountSaveDto } from '@modules/account/services/dto'; +import { Group, GroupService } from '@modules/group'; +import { FederalStateService, LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; +import { RoleService } from '@modules/role'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { UserService } from '@modules/user'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { ExternalSource, LegacySchoolDo, RoleName, RoleReference, SchoolFeatures } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { externalGroupDtoFactory, federalStateFactory, groupFactory, - roleDtoFactory, legacySchoolDoFactory, + roleDtoFactory, + roleFactory, schoolYearFactory, userDoFactory, - roleFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountSaveDto } from '@modules/account/services/dto'; -import { Group, GroupService } from '@modules/group'; -import { RoleService } from '@modules/role'; -import { RoleDto } from '@modules/role/service/dto/role.dto'; -import { FederalStateService, LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; import CryptoJS from 'crypto-js'; -import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../../dto'; import { SchoolForGroupNotFoundLoggable, UserForGroupNotFoundLoggable } from '../../../loggable'; import { OidcProvisioningService } from './oidc-provisioning.service'; @@ -101,102 +102,414 @@ describe('OidcProvisioningService', () => { }); describe('provisionExternalSchool', () => { - const setup = () => { - const systemId = 'systemId'; - const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ - externalId: 'externalId', - name: 'name', - officialSchoolNumber: 'officialSchoolNumber', - }); - const savedSchoolDO = legacySchoolDoFactory.build({ - id: 'schoolId', - externalId: 'externalId', - name: 'name', - officialSchoolNumber: 'officialSchoolNumber', - systems: [systemId], - features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + describe('when systemId is given and external school does not exist', () => { + describe('when successful', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + }); + + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = new LegacySchoolDo({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(null); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); + + return { + systemId, + externalSchoolDto, + savedSchoolDO, + }; + }; + + it('should save the correct data', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); + + await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(schoolService.save).toHaveBeenCalledWith({ ...savedSchoolDO, id: undefined }, true); + }); + + it('should save the new school', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); + + const result: LegacySchoolDo = await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(result).toEqual(savedSchoolDO); + }); }); - const existingSchoolDO = legacySchoolDoFactory.build({ - id: 'schoolId', - externalId: 'externalId', - name: 'existingName', - officialSchoolNumber: 'existingOfficialSchoolNumber', - systems: [systemId], - features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + + describe('when the external system provides a location for the school', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + location: 'Hannover', + }); + + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = new LegacySchoolDo({ + id: 'schoolId', + externalId: 'externalId', + name: 'name (Hannover)', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(null); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); + + return { + systemId, + externalSchoolDto, + savedSchoolDO, + }; + }; + + it('should append it to the school name', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); + + await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(schoolService.save).toHaveBeenCalledWith({ ...savedSchoolDO, id: undefined }, true); + }); }); - schoolService.save.mockResolvedValue(savedSchoolDO); - schoolService.getSchoolByExternalId.mockResolvedValue(null); - schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYearFactory.build()); - federalStateService.findFederalStateByName.mockResolvedValue(federalStateFactory.build()); + describe('when the external system does not provide a location for the school', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + }); - return { - systemId, - externalSchoolDto, - savedSchoolDO, - existingSchoolDO, - }; - }; + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = new LegacySchoolDo({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); - describe('when systemId is given and external school does not exist', () => { - it('should save the new school', async () => { - const { systemId, externalSchoolDto, savedSchoolDO } = setup(); + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(null); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); - const result: LegacySchoolDo = await service.provisionExternalSchool(externalSchoolDto, systemId); + return { + systemId, + externalSchoolDto, + savedSchoolDO, + }; + }; + + it('should only use the school name', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); - expect(result).toEqual(savedSchoolDO); + await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(schoolService.save).toHaveBeenCalledWith({ ...savedSchoolDO, id: undefined }, true); + }); }); }); describe('when external school already exist', () => { - it('should update the existing school', async () => { - const { systemId, externalSchoolDto, existingSchoolDO, savedSchoolDO } = setup(); + describe('when successful', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + }); - schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + const existingSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'existingName', + officialSchoolNumber: 'existingOfficialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); + + return { + systemId, + externalSchoolDto, + savedSchoolDO, + existingSchoolDO, + }; + }; - const result: LegacySchoolDo = await service.provisionExternalSchool(externalSchoolDto, systemId); + it('should update the existing school', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); - expect(result).toEqual(savedSchoolDO); + const result: LegacySchoolDo = await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(result).toEqual(savedSchoolDO); + }); }); - it('should append the new system', async () => { - const { systemId, externalSchoolDto, existingSchoolDO, savedSchoolDO } = setup(); - const otherSystemId = 'otherSystemId'; - existingSchoolDO.systems = [otherSystemId]; + describe('when the external system provides a location for the school', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + location: 'Hannover', + }); - schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'name (Hannover)', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + const existingSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'existingName', + officialSchoolNumber: 'existingOfficialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); - await service.provisionExternalSchool(externalSchoolDto, systemId); + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); - expect(schoolService.save).toHaveBeenCalledWith( - { - ...savedSchoolDO, - systems: [otherSystemId, systemId], - }, - true - ); + return { + systemId, + externalSchoolDto, + savedSchoolDO, + existingSchoolDO, + }; + }; + + it('should append it to the school name', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); + + await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(schoolService.save).toHaveBeenCalledWith(savedSchoolDO, true); + }); }); - it('should create a new system list', async () => { - const { systemId, externalSchoolDto, existingSchoolDO, savedSchoolDO } = setup(); - existingSchoolDO.systems = undefined; + describe('when the external system does not provide a location for the school', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + }); + + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + const existingSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'existingName', + officialSchoolNumber: 'existingOfficialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); - schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + return { + systemId, + externalSchoolDto, + savedSchoolDO, + existingSchoolDO, + }; + }; + + it('should only use the school name', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); - await service.provisionExternalSchool(externalSchoolDto, systemId); + await service.provisionExternalSchool(externalSchoolDto, systemId); - expect(schoolService.save).toHaveBeenCalledWith( - { - ...savedSchoolDO, - federalState: { - ...savedSchoolDO.federalState, - createdAt: expect.any(Date), - updatedAt: expect.any(Date), + expect(schoolService.save).toHaveBeenCalledWith(savedSchoolDO, true); + }); + }); + + describe('when there is a system at the school', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const otherSystemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + }); + + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [otherSystemId, systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + const existingSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'existingName', + officialSchoolNumber: 'existingOfficialSchoolNumber', + systems: [otherSystemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); + + return { + systemId, + otherSystemId, + externalSchoolDto, + savedSchoolDO, + existingSchoolDO, + }; + }; + + it('should append the new system', async () => { + const { systemId, otherSystemId, externalSchoolDto, savedSchoolDO } = setup(); + + await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(schoolService.save).toHaveBeenCalledWith( + { + ...savedSchoolDO, + systems: [otherSystemId, systemId], }, - inMaintenanceSince: expect.any(Date), - }, - true - ); + true + ); + }); + }); + + describe('when there is no system at the school yet', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + }); + + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + const existingSchoolDO = legacySchoolDoFactory.build({ + id: 'schoolId', + externalId: 'externalId', + name: 'existingName', + officialSchoolNumber: 'existingOfficialSchoolNumber', + systems: [], + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); + + return { + systemId, + externalSchoolDto, + savedSchoolDO, + existingSchoolDO, + }; + }; + + it('should create a new system list', async () => { + const { systemId, externalSchoolDto, savedSchoolDO } = setup(); + + await service.provisionExternalSchool(externalSchoolDto, systemId); + + expect(schoolService.save).toHaveBeenCalledWith(savedSchoolDO, true); + }); }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts index 66c243e6457..6d13537439b 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts @@ -1,7 +1,3 @@ -import { Injectable, UnprocessableEntityException } from '@nestjs/common'; -import { EntityId, ExternalSource, FederalStateEntity, SchoolFeatures, SchoolYearEntity } from '@shared/domain'; -import { LegacySchoolDo, RoleReference, UserDO } from '@shared/domain/domainobject'; -import { Logger } from '@src/core/logger'; import { AccountService } from '@modules/account/services/account.service'; import { AccountSaveDto } from '@modules/account/services/dto'; import { Group, GroupService, GroupUser } from '@modules/group'; @@ -10,9 +6,13 @@ import { FederalStateNames } from '@modules/legacy-school/types'; import { RoleService } from '@modules/role'; import { RoleDto } from '@modules/role/service/dto/role.dto'; import { UserService } from '@modules/user'; +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { EntityId, ExternalSource, FederalStateEntity, SchoolFeatures, SchoolYearEntity } from '@shared/domain'; +import { LegacySchoolDo, RoleReference, UserDO } from '@shared/domain/domainobject'; +import { Logger } from '@src/core/logger'; import { ObjectId } from 'bson'; import CryptoJS from 'crypto-js'; -import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { ExternalGroupDto, ExternalGroupUserDto, ExternalSchoolDto, ExternalUserDto } from '../../../dto'; import { SchoolForGroupNotFoundLoggable, UserForGroupNotFoundLoggable } from '../../../loggable'; @@ -37,7 +37,7 @@ export class OidcProvisioningService { let school: LegacySchoolDo; if (existingSchool) { school = existingSchool; - school.name = externalSchool.name; + school.name = this.getSchoolName(externalSchool); school.officialSchoolNumber = externalSchool.officialSchoolNumber ?? existingSchool.officialSchoolNumber; if (!school.systems) { school.systems = [systemId]; @@ -52,7 +52,7 @@ export class OidcProvisioningService { school = new LegacySchoolDo({ externalId: externalSchool.externalId, - name: externalSchool.name, + name: this.getSchoolName(externalSchool), officialSchoolNumber: externalSchool.officialSchoolNumber, systems: [systemId], features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], @@ -63,9 +63,18 @@ export class OidcProvisioningService { } const savedSchool: LegacySchoolDo = await this.schoolService.save(school, true); + return savedSchool; } + private getSchoolName(externalSchool: ExternalSchoolDto): string { + const schoolName: string = externalSchool.location + ? `${externalSchool.name} (${externalSchool.location})` + : externalSchool.name; + + return schoolName; + } + async provisionExternalUser(externalUser: ExternalUserDto, systemId: EntityId, schoolId?: string): Promise { let roleRefs: RoleReference[] | undefined; if (externalUser.roles) { diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts index 56f70ad0f41..bb8cc6e8d07 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts @@ -11,3 +11,4 @@ export * from './sanis-personenkontext-response'; export * from './sanis-gruppenzugehoerigkeit-response'; export * from './sanis-person-response'; export * from './sanis-sonstige-gruppenzugehoerige-response'; +export * from './sanis-anschrift-response'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-anschrift-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-anschrift-response.ts new file mode 100644 index 00000000000..6b793ba4486 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-anschrift-response.ts @@ -0,0 +1,9 @@ +export interface SanisAnschriftResponse { + adresszeile?: string; + + postleitzahl?: string; + + ort?: string; + + ortsteil?: string; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts index 258cde00a50..fa7d2846ad1 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts @@ -1,3 +1,5 @@ +import { SanisAnschriftResponse } from './sanis-anschrift-response'; + export interface SanisOrganisationResponse { id: string; @@ -6,4 +8,6 @@ export interface SanisOrganisationResponse { name: string; typ: string; + + anschrift?: SanisAnschriftResponse; } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts index 2fe68c0163b..9829e3cb930 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; +import { GroupTypes } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { GroupTypes } from '@modules/group'; import { UUID } from 'bson'; import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { @@ -56,6 +56,9 @@ describe('SanisResponseMapper', () => { name: 'schoolName', typ: 'SCHULE', kennung: 'NI_123456_NI_ashd3838', + anschrift: { + ort: 'Hannover', + }, }, personenstatus: '', gruppen: [ @@ -103,6 +106,7 @@ describe('SanisResponseMapper', () => { externalId: externalSchoolId, name: 'schoolName', officialSchoolNumber: '123456_NI_ashd3838', + location: 'Hannover', }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts index 23d9b15fbdc..ee912dd67b5 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts @@ -1,7 +1,7 @@ +import { GroupTypes } from '@modules/group'; import { Injectable } from '@nestjs/common'; import { RoleName } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { GroupTypes } from '@modules/group'; import { ExternalGroupDto, ExternalGroupUserDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { GroupRoleUnknownLoggable } from '../../loggable'; import { @@ -45,6 +45,7 @@ export class SanisResponseMapper { name: source.personenkontexte[0].organisation.name, externalId: source.personenkontexte[0].organisation.id.toString(), officialSchoolNumber, + location: source.personenkontexte[0].organisation.anschrift?.ort, }); return mapped; From 96b199f9a70c96bef259bf77a4879702b94f1b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Thu, 9 Nov 2023 09:07:49 +0100 Subject: [PATCH 09/23] fix optional parameter with regex (#4536) --- .../common-tool-validation.service.spec.ts | 36 +++++++++++++++++++ .../service/common-tool-validation.service.ts | 2 +- .../external-tool-validation.service.ts | 4 +-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts b/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts index 43a2aba1a39..d3ca547a854 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts @@ -597,6 +597,7 @@ describe('CommonToolValidationService', () => { const setup = () => { const undefinedRegex: CustomParameter = customParameterFactory.build({ name: 'undefinedRegex', + isOptional: false, scope: CustomParameterScope.SCHOOL, type: CustomParameterType.STRING, regex: undefined, @@ -629,6 +630,7 @@ describe('CommonToolValidationService', () => { const setup = () => { const validRegex: CustomParameter = customParameterFactory.build({ name: 'validRegex', + isOptional: false, scope: CustomParameterScope.SCHOOL, type: CustomParameterType.STRING, regex: '[x]', @@ -661,6 +663,7 @@ describe('CommonToolValidationService', () => { const setup = () => { const validRegex: CustomParameter = customParameterFactory.build({ name: 'validRegex', + isOptional: false, scope: CustomParameterScope.SCHOOL, type: CustomParameterType.STRING, regex: '[x]', @@ -688,6 +691,39 @@ describe('CommonToolValidationService', () => { expect(func).toThrowError('tool_param_value_regex'); }); }); + + describe('when parameter is optional and a regex is given, but the param value is undefined', () => { + const setup = () => { + const optionalRegex: CustomParameter = customParameterFactory.build({ + name: 'optionalRegex', + isOptional: true, + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + regex: '[x]', + }); + const { externalTool, schoolExternalTool } = createTools( + { + parameters: [optionalRegex], + }, + { + parameters: [{ name: 'optionalRegex', value: undefined }], + } + ); + + return { + externalTool, + schoolExternalTool, + }; + }; + + it('should return without error', () => { + const { externalTool, schoolExternalTool } = setup(); + + const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); + + expect(func).not.toThrowError('tool_param_value_regex'); + }); + }); }); }); }); diff --git a/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts b/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts index 9d315a97f34..3ee9ee7d465 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts @@ -87,7 +87,7 @@ export class CommonToolValidationService { } private checkParameterRegex(foundEntry: CustomParameterEntry, param: CustomParameter): void { - if (param.regex && !new RegExp(param.regex).test(foundEntry.value ?? '')) { + if (foundEntry.value !== undefined && param.regex && !new RegExp(param.regex).test(foundEntry.value ?? '')) { throw new ValidationError( `tool_param_value_regex: The given entry for the parameter with name ${foundEntry.name} does not fit the regex.` ); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts index 434e7fac86e..bf768a83a3e 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ValidationError } from '@shared/common'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolParameterValidationService } from './external-tool-parameter-validation.service'; @@ -11,7 +10,6 @@ export class ExternalToolValidationService { constructor( private readonly externalToolService: ExternalToolService, private readonly externalToolParameterValidationService: ExternalToolParameterValidationService, - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, private readonly externalToolLogoService: ExternalToolLogoService ) {} From 59ca98c6811e71f18383479742a742b954d478e9 Mon Sep 17 00:00:00 2001 From: Max Bischof <106820326+bischofmax@users.noreply.github.com> Date: Thu, 9 Nov 2023 12:28:26 +0100 Subject: [PATCH 10/23] BC-5702 - Add DecodeHtmlEntities decorator to caption and alternatveText response (#4528) --- .../board/controller/dto/element/file-element.response.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/server/src/modules/board/controller/dto/element/file-element.response.ts b/apps/server/src/modules/board/controller/dto/element/file-element.response.ts index 0fa23a7f735..d6c5122358f 100644 --- a/apps/server/src/modules/board/controller/dto/element/file-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/file-element.response.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { DecodeHtmlEntities } from '@shared/controller'; import { ContentElementType } from '@shared/domain'; import { TimestampsResponse } from '../timestamps.response'; @@ -9,9 +10,11 @@ export class FileElementContent { } @ApiProperty() + @DecodeHtmlEntities() caption: string; @ApiProperty() + @DecodeHtmlEntities() alternativeText: string; } From a7273a1394ddb02f51cc4b11d89af4b9b2281d2e Mon Sep 17 00:00:00 2001 From: Max Bischof <106820326+bischofmax@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:08:04 +0100 Subject: [PATCH 11/23] BC-5727 - Use etag to use preview cache (#4537) --- .../files-storage-preview.api.spec.ts | 111 +++++++++++++----- .../controller/files-storage.controller.ts | 14 ++- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts index f63eef6ac68..6ccc6d0d5ea 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts @@ -1,4 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { PreviewProducer } from '@infra/preview-generator'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; @@ -6,9 +9,6 @@ import { ExecutionContext, INestApplication, NotFoundException, StreamableFile } import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@infra/antivirus'; -import { PreviewProducer } from '@infra/preview-generator'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; @@ -63,6 +63,19 @@ class API { }; } + async getPreviewWithEtag(routeName: string, etag: string, query?: string | Record) { + const response = await request(this.app.getHttpServer()) + .get(routeName) + .query(query || {}) + .set('If-None-Match', etag); + + return { + result: response.body as StreamableFile, + error: response.body as ApiValidationError, + status: response.status, + }; + } + async getPreviewBytesRange(routeName: string, bytesRange: string, query?: string | Record) { const response = await request(this.app.getHttpServer()) .get(routeName) @@ -299,34 +312,75 @@ describe('File Controller (API) - preview', () => { return { uploadedFile }; }; - it('should return status 200 for successful download', async () => { - const { uploadedFile } = await setup(); - const query = { - ...defaultQueryParameters, - forceUpdate: false, - }; - - const response = await api.getPreview(`/file/preview/${uploadedFile.id}/${uploadedFile.name}`, query); - - expect(response.status).toEqual(200); + describe('WHEN header contains no etag', () => { + it('should return status 200 for successful download', async () => { + const { uploadedFile } = await setup(); + const query = { + ...defaultQueryParameters, + forceUpdate: false, + }; + + const response = await api.getPreview(`/file/preview/${uploadedFile.id}/${uploadedFile.name}`, query); + + expect(response.status).toEqual(200); + }); + + it('should return status 206 and required headers for the successful partial file stream download', async () => { + const { uploadedFile } = await setup(); + const query = { + ...defaultQueryParameters, + forceUpdate: false, + }; + + const response = await api.getPreviewBytesRange( + `/file/preview/${uploadedFile.id}/${uploadedFile.name}`, + 'bytes=0-', + query + ); + + expect(response.status).toEqual(206); + expect(response.headers['accept-ranges']).toMatch('bytes'); + expect(response.headers['content-range']).toMatch('bytes 0-3/4'); + expect(response.headers.etag).toMatch('testTag'); + }); }); - it('should return status 206 and required headers for the successful partial file stream download', async () => { - const { uploadedFile } = await setup(); - const query = { - ...defaultQueryParameters, - forceUpdate: false, - }; - - const response = await api.getPreviewBytesRange( - `/file/preview/${uploadedFile.id}/${uploadedFile.name}`, - 'bytes=0-', - query - ); + describe('WHEN header contains not matching etag', () => { + it('should return status 200 for successful download', async () => { + const { uploadedFile } = await setup(); + const query = { + ...defaultQueryParameters, + forceUpdate: false, + }; + const etag = 'otherTag'; + + const response = await api.getPreviewWithEtag( + `/file/preview/${uploadedFile.id}/${uploadedFile.name}`, + etag, + query + ); + + expect(response.status).toEqual(200); + }); + }); - expect(response.status).toEqual(206); - expect(response.headers['accept-ranges']).toMatch('bytes'); - expect(response.headers['content-range']).toMatch('bytes 0-3/4'); + describe('WHEN header contains matching etag', () => { + it('should return status 304', async () => { + const { uploadedFile } = await setup(); + const query = { + ...defaultQueryParameters, + forceUpdate: false, + }; + const etag = 'testTag'; + + const response = await api.getPreviewWithEtag( + `/file/preview/${uploadedFile.id}/${uploadedFile.name}`, + etag, + query + ); + + expect(response.status).toEqual(304); + }); }); }); @@ -369,6 +423,7 @@ describe('File Controller (API) - preview', () => { expect(response.status).toEqual(206); expect(response.headers['accept-ranges']).toMatch('bytes'); expect(response.headers['content-range']).toMatch('bytes 0-3/4'); + expect(response.headers.etag).toMatch('testTag'); }); }); }); diff --git a/apps/server/src/modules/files-storage/controller/files-storage.controller.ts b/apps/server/src/modules/files-storage/controller/files-storage.controller.ts index 736d69e3e29..7269336c44c 100644 --- a/apps/server/src/modules/files-storage/controller/files-storage.controller.ts +++ b/apps/server/src/modules/files-storage/controller/files-storage.controller.ts @@ -120,6 +120,7 @@ export class FilesStorageController { @ApiOperation({ summary: 'Streamable download of a preview file.' }) @ApiResponse({ status: 200, type: StreamableFile }) @ApiResponse({ status: 206, type: StreamableFile }) + @ApiResponse({ status: 304, description: 'Not Modified' }) @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 404, type: NotFoundException }) @@ -134,8 +135,9 @@ export class FilesStorageController { @Query() previewParams: PreviewParams, @Req() req: Request, @Res({ passthrough: true }) response: Response, - @Headers('Range') bytesRange?: string - ): Promise { + @Headers('Range') bytesRange?: string, + @Headers('If-None-Match') etag?: string + ): Promise { const fileResponse = await this.filesStorageUC.downloadPreview( currentUser.userId, params, @@ -143,6 +145,14 @@ export class FilesStorageController { bytesRange ); + response.set({ ETag: fileResponse.etag }); + + if (etag === fileResponse.etag) { + response.status(HttpStatus.NOT_MODIFIED); + + return undefined; + } + const streamableFile = this.streamFileToClient(req, fileResponse, response, bytesRange); return streamableFile; From f81f023ddff2194b5154e58a511f271022df33a4 Mon Sep 17 00:00:00 2001 From: sszafGCA <116172610+sszafGCA@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:52:31 +0100 Subject: [PATCH 12/23] BC-4895-stop sending emails to non existent thr @schul-cloud.org addresses (#4476) * impl of mail addresses whitelist logic --- .../src/infra/mail/interfaces/mail-config.ts | 3 ++ apps/server/src/infra/mail/mail.module.ts | 3 ++ .../src/infra/mail/mail.service.spec.ts | 49 ++++++++++++++++--- apps/server/src/infra/mail/mail.service.ts | 42 ++++++++++++++-- .../src/modules/server/server.config.ts | 7 ++- config/default.schema.json | 1 + 6 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 apps/server/src/infra/mail/interfaces/mail-config.ts diff --git a/apps/server/src/infra/mail/interfaces/mail-config.ts b/apps/server/src/infra/mail/interfaces/mail-config.ts new file mode 100644 index 00000000000..6dbb0c7864d --- /dev/null +++ b/apps/server/src/infra/mail/interfaces/mail-config.ts @@ -0,0 +1,3 @@ +export interface IMailConfig { + ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: string[]; +} diff --git a/apps/server/src/infra/mail/mail.module.ts b/apps/server/src/infra/mail/mail.module.ts index 1ca0630c44f..ee6d50d59e7 100644 --- a/apps/server/src/infra/mail/mail.module.ts +++ b/apps/server/src/infra/mail/mail.module.ts @@ -1,5 +1,7 @@ import { Module, DynamicModule } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { MailService } from './mail.service'; +import { IMailConfig } from './interfaces/mail-config'; interface MailModuleOptions { exchange: string; @@ -17,6 +19,7 @@ export class MailModule { provide: 'MAIL_SERVICE_OPTIONS', useValue: { exchange: options.exchange, routingKey: options.routingKey }, }, + ConfigService, ], exports: [MailService], }; diff --git a/apps/server/src/infra/mail/mail.service.spec.ts b/apps/server/src/infra/mail/mail.service.spec.ts index 58c0ce9336a..ebc77030252 100644 --- a/apps/server/src/infra/mail/mail.service.spec.ts +++ b/apps/server/src/infra/mail/mail.service.spec.ts @@ -1,7 +1,10 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Test, TestingModule } from '@nestjs/testing'; +import { createMock } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; import { Mail } from './mail.interface'; import { MailService } from './mail.service'; +import { IMailConfig } from './interfaces/mail-config'; describe('MailService', () => { let module: TestingModule; @@ -19,6 +22,10 @@ describe('MailService', () => { MailService, { provide: AmqpConnection, useValue: { publish: () => {} } }, { provide: 'MAIL_SERVICE_OPTIONS', useValue: mailServiceOptions }, + { + provide: ConfigService, + useValue: createMock>({ get: () => ['schul-cloud.org', 'example.com'] }), + }, ], }).compile(); @@ -34,13 +41,43 @@ describe('MailService', () => { expect(service).toBeDefined(); }); - it('should send given data to queue', async () => { - const data: Mail = { mail: { plainTextContent: 'content', subject: 'Test' }, recipients: ['test@example.com'] }; - const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish'); + describe('send', () => { + describe('when recipients array is empty', () => { + it('should not send email', async () => { + const data: Mail = { + mail: { plainTextContent: 'content', subject: 'Test' }, + recipients: ['test@schul-cloud.org'], + }; - await service.send(data); + const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish'); - const expectedParams = [mailServiceOptions.exchange, mailServiceOptions.routingKey, data, { persistent: true }]; - expect(amqpConnectionSpy).toHaveBeenCalledWith(...expectedParams); + await service.send(data); + + expect(amqpConnectionSpy).toHaveBeenCalledTimes(0); + }); + }); + describe('when sending email', () => { + it('should remove email address that have blacklisted domain and send given data to queue', async () => { + const data: Mail = { + mail: { plainTextContent: 'content', subject: 'Test' }, + recipients: ['test@schul-cloud.org', 'test@example1.com', 'test2@schul-cloud.org', 'test3@schul-cloud.org'], + cc: ['test@example.com', 'test5@schul-cloud.org', 'test6@schul-cloud.org'], + bcc: ['test7@schul-cloud.org', 'test@example2.com', 'test8@schul-cloud.org'], + replyTo: ['test@example3.com', 'test9@schul-cloud.org', 'test10@schul-cloud.org'], + }; + + const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish'); + + await service.send(data); + + expect(data.recipients).toEqual(['test@example1.com']); + expect(data.cc).toEqual([]); + expect(data.bcc).toEqual(['test@example2.com']); + expect(data.replyTo).toEqual(['test@example3.com']); + + const expectedParams = [mailServiceOptions.exchange, mailServiceOptions.routingKey, data, { persistent: true }]; + expect(amqpConnectionSpy).toHaveBeenCalledWith(...expectedParams); + }); + }); }); }); diff --git a/apps/server/src/infra/mail/mail.service.ts b/apps/server/src/infra/mail/mail.service.ts index aaf9cfacb9d..432f0746934 100644 --- a/apps/server/src/infra/mail/mail.service.ts +++ b/apps/server/src/infra/mail/mail.service.ts @@ -1,7 +1,8 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Inject, Injectable } from '@nestjs/common'; - +import { ConfigService } from '@nestjs/config'; import { Mail } from './mail.interface'; +import { IMailConfig } from './interfaces/mail-config'; interface MailServiceOptions { exchange: string; @@ -10,12 +11,47 @@ interface MailServiceOptions { @Injectable() export class MailService { + private readonly domainBlacklist: string[]; + constructor( private readonly amqpConnection: AmqpConnection, - @Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions - ) {} + @Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions, + private readonly configService: ConfigService + ) { + this.domainBlacklist = this.configService.get('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS'); + } public async send(data: Mail): Promise { + if (this.domainBlacklist.length > 0) { + data.recipients = this.filterEmailAdresses(data.recipients) as string[]; + data.cc = this.filterEmailAdresses(data.cc); + data.bcc = this.filterEmailAdresses(data.bcc); + data.replyTo = this.filterEmailAdresses(data.replyTo); + } + + if (data.recipients.length === 0) { + return; + } + await this.amqpConnection.publish(this.options.exchange, this.options.routingKey, data, { persistent: true }); } + + private filterEmailAdresses(mails: string[] | undefined): string[] | undefined { + if (mails === undefined || mails === null) { + return mails; + } + const mailWhitelist: string[] = []; + + for (const mail of mails) { + const mailDomain = this.getMailDomain(mail); + if (mailDomain && !this.domainBlacklist.includes(mailDomain)) { + mailWhitelist.push(mail); + } + } + return mailWhitelist; + } + + private getMailDomain(mail: string): string { + return mail.split('@')[1]; + } } diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index becc7ec78fc..5d1ea95cc3b 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -5,6 +5,7 @@ import type { IAccountConfig } from '@modules/account'; import type { IFilesStorageClientConfig } from '@modules/files-storage-client'; import type { IUserConfig } from '@modules/user'; import type { ICommonCartridgeConfig } from '@modules/learnroom/common-cartridge'; +import { IMailConfig } from '@src/infra/mail/interfaces/mail-config'; export enum NodeEnvType { TEST = 'test', @@ -19,7 +20,8 @@ export interface IServerConfig IFilesStorageClientConfig, IAccountConfig, IIdentityManagementConfig, - ICommonCartridgeConfig { + ICommonCartridgeConfig, + IMailConfig { NODE_ENV: string; SC_DOMAIN: string; } @@ -39,6 +41,9 @@ const config: IServerConfig = { FEATURE_IDENTITY_MANAGEMENT_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') as boolean, + ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: (Configuration.get('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS') as string) + .split(',') + .map((domain) => domain.trim()), }; export const serverConfig = () => config; diff --git a/config/default.schema.json b/config/default.schema.json index a34d8e899ad..bd637b719e9 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -177,6 +177,7 @@ }, "ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS": { "type": "string", + "default":"", "description": "Add custom domain to the list of blocked domains (comma separated list)." }, "FEATURE_TSP_AUTO_CONSENT_ENABLED": { From 4a7cb7174cd4dd97eb3d4a180608024a37de34f4 Mon Sep 17 00:00:00 2001 From: Cedric Evers <12080057+CeEv@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:43:51 +0100 Subject: [PATCH 13/23] BC-5662 Add linter rule for deep module imports (#4538) --- .eslintrc.js | 29 +- apps/server/src/apps/server.app.ts | 2 +- apps/server/src/modules/account/index.ts | 2 +- .../src/modules/account/services/index.ts | 1 + .../modules/user/service/user.service.spec.ts | 9 +- package-lock.json | 1228 +++++++++++------ package.json | 2 +- 7 files changed, 873 insertions(+), 400 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 008320274fb..a1851003d0d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -65,22 +65,24 @@ module.exports = { overrides: [ { files: ['apps/**/*.ts'], + env: { + node: true, + es6: true, + }, parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint/eslint-plugin'], + parserOptions: { + project: 'apps/server/tsconfig.lint.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin', 'import'], extends: [ 'airbnb-typescript/base', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'prettier', 'plugin:promise/recommended', + 'plugin:import/typescript', ], - parserOptions: { - project: 'apps/server/tsconfig.lint.json', - }, - env: { - node: true, - es6: true, - }, rules: { 'import/no-unresolved': 'off', // better handled by ts resolver 'import/no-extraneous-dependencies': 'off', // better handles by ts resolver @@ -98,6 +100,17 @@ module.exports = { allowSingleExtends: true, }, ], + '@typescript-eslint/no-restricted-imports': [ + 'warn', + { + patterns: [ + { + group: ['@infra/*/*', '@modules/*/*', '!*.module'], + message: 'Do not deep import from a module', + }, + ], + }, + ], }, overrides: [ { diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 9538d6c8515..6322bcd568f 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -7,7 +7,7 @@ import { ExpressAdapter } from '@nestjs/platform-express'; import { enableOpenApiDocs } from '@shared/controller/swagger'; import { Mail, MailService } from '@infra/mail'; import { LegacyLogger, Logger } from '@src/core/logger'; -import { AccountService } from '@modules/account/services/account.service'; +import { AccountService } from '@modules/account'; import { TeamService } from '@modules/teams/service/team.service'; import { AccountValidationService } from '@modules/account/services/account.validation.service'; import { AccountUc } from '@modules/account/uc/account.uc'; diff --git a/apps/server/src/modules/account/index.ts b/apps/server/src/modules/account/index.ts index 2fa0bcc2334..7db4a66a657 100644 --- a/apps/server/src/modules/account/index.ts +++ b/apps/server/src/modules/account/index.ts @@ -1,3 +1,3 @@ export * from './account.module'; export * from './account-config'; -export { AccountService } from './services'; +export { AccountService, AccountDto } from './services'; diff --git a/apps/server/src/modules/account/services/index.ts b/apps/server/src/modules/account/services/index.ts index 72778be1f1e..2fc3f3a3246 100644 --- a/apps/server/src/modules/account/services/index.ts +++ b/apps/server/src/modules/account/services/index.ts @@ -1 +1,2 @@ export * from './account.service'; +export { AccountDto } from './dto'; 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 223fa1f0c88..f65d02c13a5 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -7,12 +7,11 @@ import { UserDO } from '@shared/domain/domainobject/user.do'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { roleFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountDto } from '@modules/account/services/dto'; -import { RoleService } from '@modules/role/service/role.service'; -import { UserService } from '@modules/user/service/user.service'; -import { UserDto } from '@modules/user/uc/dto/user.dto'; +import { AccountService, AccountDto } from '@modules/account'; +import { RoleService } from '@modules/role'; import { OauthCurrentUser } from '@modules/authentication/interface'; +import { UserDto } from '../uc/dto/user.dto'; +import { UserService } from './user.service'; import { UserQuery } from './user-query.type'; describe('UserService', () => { diff --git a/package-lock.json b/package-lock.json index 5f895871520..bf63523c91a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,7 +178,7 @@ "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-jest": "^27.1.6", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-no-only-tests": "^3.1.0", @@ -6866,21 +6866,33 @@ "node": ">=6.0" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -6914,15 +6926,73 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7750,12 +7820,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9283,6 +9354,19 @@ "node": ">=0.8" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -9293,10 +9377,11 @@ } }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -9708,35 +9793,49 @@ } }, "node_modules/es-abstract": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", - "integrity": "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dependencies": { - "call-bind": "^1.0.2", + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "unbox-primitive": "^1.0.2" + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -9751,6 +9850,28 @@ "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", "dev": true }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -10441,13 +10562,14 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "dependencies": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -10533,16 +10655,20 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0" + "debug": "^3.2.7" }, "engines": { "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/eslint-module-utils/node_modules/debug": { @@ -10555,24 +10681,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "engines": { "node": ">=4" @@ -10582,12 +10712,12 @@ } }, "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { @@ -10614,11 +10744,14 @@ "json5": "lib/cli.js" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } }, "node_modules/eslint-plugin-import/node_modules/strip-bom": { "version": "3.0.0", @@ -10630,13 +10763,13 @@ } }, "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } @@ -11795,18 +11928,6 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fishery": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/fishery/-/fishery-2.2.2.tgz", @@ -12149,19 +12270,22 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -12233,13 +12357,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12355,6 +12480,20 @@ "node": ">=4" } }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", @@ -12515,6 +12654,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -12558,6 +12698,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -12615,6 +12766,17 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -12987,12 +13149,12 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -13052,6 +13214,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -13129,11 +13304,11 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13372,15 +13547,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -16083,19 +16254,6 @@ "node": ">=6.11.5" } }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -18632,9 +18790,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -18678,15 +18836,44 @@ "node": ">= 0.4" } }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -18995,30 +19182,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -19031,15 +19194,6 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/package-hash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", @@ -19207,15 +19361,6 @@ "node": ">= 0.4.0" } }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -20616,13 +20761,13 @@ "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -20858,11 +21003,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -21524,6 +21669,28 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -21833,6 +22000,33 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -22420,27 +22614,43 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23803,6 +24013,67 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -24388,16 +24659,15 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -29984,21 +30254,30 @@ "@babel/runtime-corejs3": "^7.10.2" } }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" } }, @@ -30023,15 +30302,55 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, + "array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, "array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" } }, "asap": { @@ -30702,12 +31021,13 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "call-me-maybe": { @@ -31898,6 +32218,16 @@ } } }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -31905,10 +32235,11 @@ "dev": true }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "requires": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } @@ -32243,35 +32574,49 @@ } }, "es-abstract": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", - "integrity": "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "requires": { - "call-bind": "^1.0.2", + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "unbox-primitive": "^1.0.2" + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" } }, "es-module-lexer": { @@ -32280,6 +32625,25 @@ "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", "dev": true }, + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -32842,13 +33206,14 @@ "requires": {} }, "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "requires": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" }, "dependencies": { "debug": { @@ -32908,13 +33273,12 @@ } }, "eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0" + "debug": "^3.2.7" }, "dependencies": { "debug": { @@ -32929,33 +33293,37 @@ } }, "eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "doctrine": { @@ -32976,10 +33344,10 @@ "minimist": "^1.2.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "strip-bom": { @@ -32989,13 +33357,13 @@ "dev": true }, "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "dev": true, "requires": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } @@ -33721,15 +34089,6 @@ "pkg-dir": "^4.1.0" } }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, "fishery": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/fishery/-/fishery-2.2.2.tgz", @@ -33975,19 +34334,19 @@ } }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, "functional-red-black-tree": { @@ -34038,13 +34397,14 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-package-type": { @@ -34121,6 +34481,14 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "requires": { + "define-properties": "^1.1.3" + } + }, "globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", @@ -34256,6 +34624,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -34284,6 +34653,11 @@ "get-intrinsic": "^1.1.1" } }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -34322,6 +34696,14 @@ } } }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -34601,12 +34983,12 @@ } }, "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, @@ -34645,6 +35027,16 @@ "has-tostringtag": "^1.0.0" } }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -34687,11 +35079,11 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "requires": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "is-date-object": { @@ -34837,15 +35229,11 @@ } }, "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" } }, "is-typedarray": { @@ -36935,16 +37323,6 @@ "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", "dev": true }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -38968,9 +39346,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" }, "object-keys": { "version": "1.1.1", @@ -38999,15 +39377,38 @@ "es-abstract": "^1.19.1" } }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "on-headers": { @@ -39231,26 +39632,6 @@ "yocto-queue": "^0.1.0" } }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - }, - "dependencies": { - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - } - } - }, "p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -39260,12 +39641,6 @@ "aggregate-error": "^3.0.0" } }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true - }, "package-hash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", @@ -39388,12 +39763,6 @@ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -40425,13 +40794,13 @@ "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" } }, "regexpp": { @@ -40603,11 +40972,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "requires": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -41102,6 +41471,24 @@ "tslib": "^2.1.0" } }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + } + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -41352,6 +41739,27 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -41838,24 +42246,34 @@ } } }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "strip-ansi": { @@ -42855,6 +43273,49 @@ } } }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -43325,16 +43786,15 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "requires": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, "window-size": { diff --git a/package.json b/package.json index 6afd927c17c..53a0752ee10 100644 --- a/package.json +++ b/package.json @@ -261,7 +261,7 @@ "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-jest": "^27.1.6", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-no-only-tests": "^3.1.0", From a407c719a8471a8a8823e2dfa2d94e1b452ccb34 Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Fri, 10 Nov 2023 13:19:23 +0100 Subject: [PATCH 14/23] N21-1029 renaming of collections (#4547) * renames some collections: user_login_migrations -> user-login-migrations external_tools -> external-tools context_external_tools -> context-external-tools school_external_tools -> school-external-tools --- .../templates/configmap_file_init.yml.j2 | 12 ++--- .../allowed-authorization-object-type.enum.ts | 4 +- .../entity/context-external-tool.entity.ts | 2 +- .../entity/external-tool.entity.ts | 2 +- .../entity/school-external-tool.entity.ts | 2 +- .../entity/user-login-migration.entity.ts | 2 +- ...tools.json => context-external-tools.json} | 0 ...xternal_tools.json => external-tools.json} | 0 backup/setup/migrations.json | 11 +++++ ..._tools.json => school-external-tools.json} | 0 backup/setup/user-login-migrations.json | 1 + ...tool-and-user-login-migration-renamings.js | 44 +++++++++++++++++++ src/services/school/model.js | 2 +- 13 files changed, 69 insertions(+), 13 deletions(-) rename backup/setup/{context_external_tools.json => context-external-tools.json} (100%) rename backup/setup/{external_tools.json => external-tools.json} (100%) rename backup/setup/{school_external_tools.json => school-external-tools.json} (100%) create mode 100644 backup/setup/user-login-migrations.json create mode 100644 migrations/1699529266062-tool-and-user-login-migration-renamings.js diff --git a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 index a3e5459077f..654d4152b95 100644 --- a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 @@ -390,8 +390,8 @@ data: }' # Add Bettermarks' tools configuration as an external tool - # (stored in the 'external_tools' collection) that uses OAuth. - mongosh $DATABASE__URL --eval 'db.external_tools.replaceOne( + # (stored in the 'external-tools' collection) that uses OAuth. + mongosh $DATABASE__URL --eval 'db.external-tools.replaceOne( { "name": "bettermarks", "config_type": "oauth2" @@ -486,9 +486,9 @@ data: echo "POSTed nextcloud to hydra." # Add Nextcloud' tools configuration as an external tool - # (stored in the 'external_tools' collection) that uses OAuth. - echo "Inserting nextcloud to external_tools..." - mongosh $DATABASE__URL --eval 'db.external_tools.update( + # (stored in the 'external-tools' collection) that uses OAuth. + echo "Inserting nextcloud to external-tools..." + mongosh $DATABASE__URL --eval 'db.external-tools.update( { "name": "nextcloud", "config_type": "oauth2" @@ -512,7 +512,7 @@ data: "upsert": true } );' - echo "Inserted nextcloud to external_tools." + echo "Inserted nextcloud to external-tools." echo "Nextcloud config data init performed successfully." fi diff --git a/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts b/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts index f36ca235af1..01f24b21985 100644 --- a/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts +++ b/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts @@ -7,7 +7,7 @@ export enum AuthorizableReferenceType { 'Lesson' = 'lessons', 'Team' = 'teams', 'Submission' = 'submissions', - 'SchoolExternalToolEntity' = 'school_external_tools', + 'SchoolExternalToolEntity' = 'school-external-tools', 'BoardNode' = 'boardnodes', - 'ContextExternalToolEntity' = 'context_external_tools', + 'ContextExternalToolEntity' = 'context-external-tools', } diff --git a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts index 6fdbad2b3af..aebab1a8d5d 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts @@ -18,7 +18,7 @@ export interface IContextExternalToolProperties { toolVersion: number; } -@Entity({ tableName: 'context_external_tools' }) +@Entity({ tableName: 'context-external-tools' }) export class ContextExternalToolEntity extends BaseEntityWithTimestamps { @ManyToOne() schoolTool: SchoolExternalToolEntity; diff --git a/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts b/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts index 3bd3ed9c30d..481ed3b7c2d 100644 --- a/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts +++ b/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts @@ -6,7 +6,7 @@ import { BasicToolConfigEntity, Lti11ToolConfigEntity, Oauth2ToolConfigEntity } export type IExternalToolProperties = Readonly>; -@Entity({ tableName: 'external_tools' }) +@Entity({ tableName: 'external-tools' }) export class ExternalToolEntity extends BaseEntityWithTimestamps { @Unique() @Property() diff --git a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts index a1682e3b7cd..fc7f6703d05 100644 --- a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts +++ b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts @@ -11,7 +11,7 @@ export interface ISchoolExternalToolProperties { toolVersion: number; } -@Entity({ tableName: 'school_external_tools' }) +@Entity({ tableName: 'school-external-tools' }) export class SchoolExternalToolEntity extends BaseEntityWithTimestamps { @ManyToOne() tool: ExternalToolEntity; diff --git a/apps/server/src/shared/domain/entity/user-login-migration.entity.ts b/apps/server/src/shared/domain/entity/user-login-migration.entity.ts index 2daf9707f3c..b79b3d862c3 100644 --- a/apps/server/src/shared/domain/entity/user-login-migration.entity.ts +++ b/apps/server/src/shared/domain/entity/user-login-migration.entity.ts @@ -5,7 +5,7 @@ import { BaseEntityWithTimestamps } from './base.entity'; export type IUserLoginMigration = Readonly>; -@Entity({ tableName: 'user_login_migrations' }) +@Entity({ tableName: 'user-login-migrations' }) export class UserLoginMigrationEntity extends BaseEntityWithTimestamps { @OneToOne(() => SchoolEntity, undefined, { nullable: false }) school: SchoolEntity; diff --git a/backup/setup/context_external_tools.json b/backup/setup/context-external-tools.json similarity index 100% rename from backup/setup/context_external_tools.json rename to backup/setup/context-external-tools.json diff --git a/backup/setup/external_tools.json b/backup/setup/external-tools.json similarity index 100% rename from backup/setup/external_tools.json rename to backup/setup/external-tools.json diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 90fcee0baa3..e00ca430152 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -339,5 +339,16 @@ "$date": "2023-10-26T13:06:27.322Z" }, "__v": 0 + }, + { + "_id": { + "$oid": "654cc2326b83f786c4227b21" + }, + "state": "up", + "name": "tool-and-user-login-migration-renamings", + "createdAt": { + "$date": "2023-11-09T11:27:46.062Z" + }, + "__v": 0 } ] diff --git a/backup/setup/school_external_tools.json b/backup/setup/school-external-tools.json similarity index 100% rename from backup/setup/school_external_tools.json rename to backup/setup/school-external-tools.json diff --git a/backup/setup/user-login-migrations.json b/backup/setup/user-login-migrations.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/backup/setup/user-login-migrations.json @@ -0,0 +1 @@ +[] diff --git a/migrations/1699529266062-tool-and-user-login-migration-renamings.js b/migrations/1699529266062-tool-and-user-login-migration-renamings.js new file mode 100644 index 00000000000..8b2480a41e4 --- /dev/null +++ b/migrations/1699529266062-tool-and-user-login-migration-renamings.js @@ -0,0 +1,44 @@ +const mongoose = require('mongoose'); +const { info, error } = require('../src/logger'); + +const { connect, close } = require('../src/utils/database'); + +async function renameCollection(oldName, newName) { + try { + await mongoose.connection.collection(oldName).rename(newName); + info(`Renamed collection ${oldName} to ${newName}`); + } catch (err) { + error(`Error renaming collection ${oldName} to ${newName}: ${err.message}`); + throw err; + } +} + +module.exports = { + up: async function up() { + await connect(); + + await renameCollection('user_login_migrations', 'user-login-migrations'); + + await renameCollection('external_tools', 'external-tools'); + + await renameCollection('context_external_tools', 'context-external-tools'); + + await renameCollection('school_external_tools', 'school-external-tools'); + + await close(); + }, + + down: async function down() { + await connect(); + + await renameCollection('user-login-migrations', 'user_login_migrations'); + + await renameCollection('external-tools', 'external_tools'); + + await renameCollection('context-external-tools', 'context_external_tools'); + + await renameCollection('school-external-tools', 'school_external_tools'); + + await close(); + }, +}; diff --git a/src/services/school/model.js b/src/services/school/model.js index 0ec931e4191..787f7b55348 100644 --- a/src/services/school/model.js +++ b/src/services/school/model.js @@ -177,7 +177,7 @@ const gradeLevelSchema = new Schema({ }); const schoolModel = mongoose.model('school', schoolSchema); -const userLoginMigrationModel = mongoose.model('userLoginMigration', userLoginMigrationSchema, 'user_login_migrations'); +const userLoginMigrationModel = mongoose.model('userLoginMigration', userLoginMigrationSchema, 'user-login-migrations'); const schoolGroupModel = mongoose.model('schoolGroup', schoolGroupSchema); const yearModel = mongoose.model('year', yearSchema); const gradeLevelModel = mongoose.model('gradeLevel', gradeLevelSchema); From 249454c2773e5583ac2ea64b567d17b00767b47c Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Fri, 10 Nov 2023 15:55:39 +0100 Subject: [PATCH 15/23] N21-1029 bugfix renaming of collections (#4551) * changed rename to aggregate and drop --- ...tool-and-user-login-migration-renamings.js | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/migrations/1699529266062-tool-and-user-login-migration-renamings.js b/migrations/1699529266062-tool-and-user-login-migration-renamings.js index 8b2480a41e4..e1b0695b444 100644 --- a/migrations/1699529266062-tool-and-user-login-migration-renamings.js +++ b/migrations/1699529266062-tool-and-user-login-migration-renamings.js @@ -1,14 +1,23 @@ const mongoose = require('mongoose'); const { info, error } = require('../src/logger'); - const { connect, close } = require('../src/utils/database'); -async function renameCollection(oldName, newName) { +async function aggregateAndDropCollection(oldName, newName) { try { - await mongoose.connection.collection(oldName).rename(newName); - info(`Renamed collection ${oldName} to ${newName}`); + const { connection } = mongoose; + + // Aggregation pipeline for copying the documents + const pipeline = [{ $match: {} }, { $out: newName }]; + + // Copy documents from the old collection to the new collection + await connection.collection(oldName).aggregate(pipeline).toArray(); + info(`Aggregated and copied documents from ${oldName} to ${newName}`); + + // Delete old collection + await connection.collection(oldName).drop(); + info(`Dropped collection ${oldName}`); } catch (err) { - error(`Error renaming collection ${oldName} to ${newName}: ${err.message}`); + error(`Error aggregating, copying, and deleting collection ${oldName} to ${newName}: ${err.message}`); throw err; } } @@ -17,13 +26,10 @@ module.exports = { up: async function up() { await connect(); - await renameCollection('user_login_migrations', 'user-login-migrations'); - - await renameCollection('external_tools', 'external-tools'); - - await renameCollection('context_external_tools', 'context-external-tools'); - - await renameCollection('school_external_tools', 'school-external-tools'); + await aggregateAndDropCollection('user_login_migrations', 'user-login-migrations'); + await aggregateAndDropCollection('external_tools', 'external-tools'); + await aggregateAndDropCollection('context_external_tools', 'context-external-tools'); + await aggregateAndDropCollection('school_external_tools', 'school-external-tools'); await close(); }, @@ -31,13 +37,10 @@ module.exports = { down: async function down() { await connect(); - await renameCollection('user-login-migrations', 'user_login_migrations'); - - await renameCollection('external-tools', 'external_tools'); - - await renameCollection('context-external-tools', 'context_external_tools'); - - await renameCollection('school-external-tools', 'school_external_tools'); + await aggregateAndDropCollection('user-login-migrations', 'user_login_migrations'); + await aggregateAndDropCollection('external-tools', 'external_tools'); + await aggregateAndDropCollection('context-external-tools', 'context_external_tools'); + await aggregateAndDropCollection('school-external-tools', 'school_external_tools'); await close(); }, From 034cab840ab347aa621248cc47e1e7317631e4b7 Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Mon, 13 Nov 2023 10:13:15 +0100 Subject: [PATCH 16/23] N21-1456 improves oauth error handling (#4539) --- .../loggable/axios-error.loggable.spec.ts | 32 ++ .../error/loggable/axios-error.loggable.ts | 20 + apps/server/src/core/error/loggable/index.ts | 2 + .../hydra/hydra.adapter.spec.ts | 391 +++++++++++++----- .../oauth-provider/hydra/hydra.adapter.ts | 37 +- ...ra-oauth-failed-loggable-exception.spec.ts | 35 ++ .../hydra-oauth-failed-loggable-exception.ts | 8 + .../infra/oauth-provider/loggable/index.ts | 1 + .../src/modules/oauth/loggable/index.ts | 1 + .../modules/oauth/loggable/oauth-sso.error.ts | 4 + .../token-request-loggable-exception.spec.ts | 34 ++ .../token-request-loggable-exception.ts | 8 + .../service/oauth-adapter.service.spec.ts | 61 ++- .../oauth/service/oauth-adapter.service.ts | 11 +- .../testing/factory/axios-error.factory.ts | 28 ++ .../src/shared/testing/factory/index.ts | 1 + 16 files changed, 549 insertions(+), 125 deletions(-) create mode 100644 apps/server/src/core/error/loggable/axios-error.loggable.spec.ts create mode 100644 apps/server/src/core/error/loggable/axios-error.loggable.ts create mode 100644 apps/server/src/core/error/loggable/index.ts create mode 100644 apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.spec.ts create mode 100644 apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.ts create mode 100644 apps/server/src/infra/oauth-provider/loggable/index.ts create mode 100644 apps/server/src/modules/oauth/loggable/token-request-loggable-exception.spec.ts create mode 100644 apps/server/src/modules/oauth/loggable/token-request-loggable-exception.ts create mode 100644 apps/server/src/shared/testing/factory/axios-error.factory.ts diff --git a/apps/server/src/core/error/loggable/axios-error.loggable.spec.ts b/apps/server/src/core/error/loggable/axios-error.loggable.spec.ts new file mode 100644 index 00000000000..f2b480a4bf7 --- /dev/null +++ b/apps/server/src/core/error/loggable/axios-error.loggable.spec.ts @@ -0,0 +1,32 @@ +import { axiosErrorFactory } from '@shared/testing/factory'; +import { AxiosError } from 'axios'; +import { AxiosErrorLoggable } from './axios-error.loggable'; + +describe(AxiosErrorLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const error = { + error: 'invalid_request', + }; + const type = 'mockType'; + const axiosError: AxiosError = axiosErrorFactory.withError(error).build(); + + const axiosErrorLoggable = new AxiosErrorLoggable(axiosError, type); + + return { axiosErrorLoggable, error, axiosError }; + }; + + it('should return error log message', () => { + const { axiosErrorLoggable, error, axiosError } = setup(); + + const result = axiosErrorLoggable.getLogMessage(); + + expect(result).toEqual({ + type: 'mockType', + message: axiosError.message, + data: JSON.stringify(error), + stack: 'mockStack', + }); + }); + }); +}); diff --git a/apps/server/src/core/error/loggable/axios-error.loggable.ts b/apps/server/src/core/error/loggable/axios-error.loggable.ts new file mode 100644 index 00000000000..29e6ad32dad --- /dev/null +++ b/apps/server/src/core/error/loggable/axios-error.loggable.ts @@ -0,0 +1,20 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { AxiosError } from 'axios'; + +export class AxiosErrorLoggable extends HttpException implements Loggable { + constructor(private readonly axiosError: AxiosError, protected readonly type: string) { + super(JSON.stringify(axiosError.response?.data), axiosError.status ?? HttpStatus.INTERNAL_SERVER_ERROR, { + cause: axiosError.cause, + }); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: this.axiosError.message, + type: this.type, + data: JSON.stringify(this.axiosError.response?.data), + stack: this.axiosError.stack, + }; + } +} diff --git a/apps/server/src/core/error/loggable/index.ts b/apps/server/src/core/error/loggable/index.ts new file mode 100644 index 00000000000..0470cbee690 --- /dev/null +++ b/apps/server/src/core/error/loggable/index.ts @@ -0,0 +1,2 @@ +export * from './error.loggable'; +export * from './axios-error.loggable'; diff --git a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts index 7e30c5668c7..2a373195bc6 100644 --- a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts +++ b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts @@ -1,7 +1,5 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { HttpService } from '@nestjs/axios'; -import { Test, TestingModule } from '@nestjs/testing'; import { AcceptConsentRequestBody, AcceptLoginRequestBody, @@ -12,11 +10,15 @@ import { ProviderRedirectResponse, RejectRequestBody, } from '@infra/oauth-provider/dto'; +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; import { axiosResponseFactory } from '@shared/testing'; -import { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'; -import { of } from 'rxjs'; +import { axiosErrorFactory } from '@shared/testing/factory'; +import { AxiosError, AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'; +import { of, throwError } from 'rxjs'; +import { ProviderConsentSessionResponse } from '../dto'; +import { HydraOauthFailedLoggableException } from '../loggable'; import { HydraAdapter } from './hydra.adapter'; -import { ProviderConsentSessionResponse } from '../dto/response/consent-session.response'; import resetAllMocks = jest.resetAllMocks; class HydraAdapterSpec extends HydraAdapter { @@ -66,100 +68,66 @@ describe('HydraService', () => { }); describe('request', () => { - it('should return data when called with all parameters', async () => { - const data: { test: string } = { - test: 'data', - }; - - httpService.request.mockReturnValue(of(createAxiosResponse(data))); - - const result: { test: string } = await service.requestSpec( - 'GET', - 'testUrl', - { dataKey: 'dataValue' }, - { headerKey: 'headerValue' } - ); + describe('when called with all parameters', () => { + const setup = () => { + const data: { test: string } = { + test: 'data', + }; - expect(result).toEqual(data); - expect(httpService.request).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'testUrl', - method: 'GET', - headers: { - 'X-Forwarded-Proto': 'https', - headerKey: 'headerValue', - }, - data: { dataKey: 'dataValue' }, - }) - ); - }); + httpService.request.mockReturnValue(of(createAxiosResponse(data))); - it('should return data when called with only necessary parameters', async () => { - const data: { test: string } = { - test: 'data', + return { + data, + }; }; - httpService.request.mockReturnValue(of(createAxiosResponse(data))); - - const result: { test: string } = await service.requestSpec('GET', 'testUrl'); - - expect(result).toEqual(data); - expect(httpService.request).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'testUrl', - method: 'GET', - headers: { - 'X-Forwarded-Proto': 'https', - }, - }) - ); - }); - }); - - describe('Client Flow', () => { - describe('listOAuth2Clients', () => { - it('should list all oauth2 clients', async () => { - const data: ProviderOauthClient[] = [ - { - client_id: 'client1', - }, - { - client_id: 'client2', - }, - ]; - - httpService.request.mockReturnValue(of(createAxiosResponse(data))); + it('should return data', async () => { + const { data } = setup(); - const result: ProviderOauthClient[] = await service.listOAuth2Clients(); + const result: { test: string } = await service.requestSpec( + 'GET', + 'testUrl', + { dataKey: 'dataValue' }, + { headerKey: 'headerValue' } + ); expect(result).toEqual(data); expect(httpService.request).toHaveBeenCalledWith( expect.objectContaining({ - url: `${hydraUri}/clients`, + url: 'testUrl', method: 'GET', headers: { 'X-Forwarded-Proto': 'https', + headerKey: 'headerValue', }, + data: { dataKey: 'dataValue' }, }) ); }); + }); - it('should list all oauth2 clients within parameters', async () => { - const data: ProviderOauthClient[] = [ - { - client_id: 'client1', - owner: 'clientOwner', - }, - ]; + describe('when called with only necessary parameters', () => { + const setup = () => { + const data: { test: string } = { + test: 'data', + }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); - const result: ProviderOauthClient[] = await service.listOAuth2Clients(1, 0, 'client1', 'clientOwner'); + return { + data, + }; + }; + + it('should return data', async () => { + const { data } = setup(); + + const result: { test: string } = await service.requestSpec('GET', 'testUrl'); expect(result).toEqual(data); expect(httpService.request).toHaveBeenCalledWith( expect.objectContaining({ - url: `${hydraUri}/clients?limit=1&offset=0&client_name=client1&owner=clientOwner`, + url: 'testUrl', method: 'GET', headers: { 'X-Forwarded-Proto': 'https', @@ -169,13 +137,130 @@ describe('HydraService', () => { }); }); + describe('when error occurs', () => { + describe('when error is an axios error', () => { + const setup = () => { + const error = { + error: 'invalid_request', + }; + const axiosError: AxiosError = axiosErrorFactory.withError(error).build({}); + + httpService.request.mockReturnValueOnce(throwError(() => axiosError)); + + return { + axiosError, + }; + }; + + it('should throw hydra oauth loggable exception', async () => { + const { axiosError } = setup(); + + await expect(service.listOAuth2Clients()).rejects.toThrow(new HydraOauthFailedLoggableException(axiosError)); + }); + }); + + describe('when error is any other error', () => { + const setup = () => { + httpService.request.mockReturnValueOnce(throwError(() => new Error('unknown error'))); + }; + + it('should throw the error', async () => { + setup(); + + await expect(service.listOAuth2Clients()).rejects.toThrow(new Error('unknown error')); + }); + }); + }); + }); + + describe('Client Flow', () => { + describe('listOAuth2Clients', () => { + describe('when only clientIds are given', () => { + const setup = () => { + const data: ProviderOauthClient[] = [ + { + client_id: 'client1', + }, + { + client_id: 'client2', + }, + ]; + + httpService.request.mockReturnValue(of(createAxiosResponse(data))); + + return { + data, + }; + }; + + it('should list all oauth2 clients', async () => { + const { data } = setup(); + + const result: ProviderOauthClient[] = await service.listOAuth2Clients(); + + expect(result).toEqual(data); + expect(httpService.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: `${hydraUri}/clients`, + method: 'GET', + headers: { + 'X-Forwarded-Proto': 'https', + }, + }) + ); + }); + }); + + describe('when clientId and other parameters are given', () => { + const setup = () => { + const data: ProviderOauthClient[] = [ + { + client_id: 'client1', + owner: 'clientOwner', + }, + ]; + + httpService.request.mockReturnValue(of(createAxiosResponse(data))); + + return { + data, + }; + }; + + it('should list all oauth2 clients within parameters', async () => { + const { data } = setup(); + + const result: ProviderOauthClient[] = await service.listOAuth2Clients(1, 0, 'client1', 'clientOwner'); + + expect(result).toEqual(data); + expect(httpService.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: `${hydraUri}/clients?limit=1&offset=0&client_name=client1&owner=clientOwner`, + method: 'GET', + headers: { + 'X-Forwarded-Proto': 'https', + }, + }) + ); + }); + }); + }); + describe('getOAuth2Client', () => { - it('should get oauth2 client', async () => { + const setup = () => { const data: ProviderOauthClient = { client_id: 'client', }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); + return { + data, + }; + }; + + it('should get oauth2 client', async () => { + const { data } = setup(); + const result: ProviderOauthClient = await service.getOAuth2Client('clientId'); expect(result).toEqual(data); @@ -192,12 +277,20 @@ describe('HydraService', () => { }); describe('createOAuth2Client', () => { - it('should create oauth2 client', async () => { + const setup = () => { const data: ProviderOauthClient = { client_id: 'client', }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); + return { + data, + }; + }; + + it('should create oauth2 client', async () => { + const { data } = setup(); + const result: ProviderOauthClient = await service.createOAuth2Client(data); expect(result).toEqual(data); @@ -215,12 +308,20 @@ describe('HydraService', () => { }); describe('updateOAuth2Client', () => { - it('should update oauth2 client', async () => { + const setup = () => { const data: ProviderOauthClient = { client_id: 'client', }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); + return { + data, + }; + }; + + it('should update oauth2 client', async () => { + const { data } = setup(); + const result: ProviderOauthClient = await service.updateOAuth2Client('clientId', data); expect(result).toEqual(data); @@ -238,8 +339,12 @@ describe('HydraService', () => { }); describe('deleteOAuth2Client', () => { - it('should delete oauth2 client', async () => { + const setup = () => { httpService.request.mockReturnValue(of(createAxiosResponse({}))); + }; + + it('should delete oauth2 client', async () => { + setup(); await service.deleteOAuth2Client('clientId'); @@ -268,26 +373,30 @@ describe('HydraService', () => { }); describe('getConsentRequest', () => { - it('should make http request', async () => { - // Arrange + const setup = () => { const config: AxiosRequestConfig = { method: 'GET', url: `${hydraUri}/oauth2/auth/requests/consent?consent_challenge=${challenge}`, }; httpService.request.mockReturnValue(of(createAxiosResponse({ challenge }))); - // Act + return { + config, + }; + }; + + it('should make http request', async () => { + const { config } = setup(); + const result: ProviderConsentResponse = await service.getConsentRequest(challenge); - // Assert expect(result.challenge).toEqual(challenge); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); }); describe('acceptConsentRequest', () => { - it('should make http request', async () => { - // Arrange + const setup = () => { const body: AcceptConsentRequestBody = { grant_scope: ['offline', 'openid'], }; @@ -301,18 +410,25 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); - // Act + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should make http request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.acceptConsentRequest(challenge, body); - // Assert expect(result.redirect_to).toEqual(expectedRedirectTo); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); }); describe('rejectConsentRequest', () => { - it('should make http request', async () => { - // Arrange + const setup = () => { const body: RejectRequestBody = { error: 'error', }; @@ -326,20 +442,36 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); - // Act + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should make http request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.rejectConsentRequest(challenge, body); - // Assert expect(result.redirect_to).toEqual(expectedRedirectTo); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); }); describe('listConsentSessions', () => { - it('should list all consent sessions', async () => { + const setup = () => { const response: ProviderConsentSessionResponse[] = [{ consent_request: { challenge: 'challenge' } }]; httpService.request.mockReturnValue(of(createAxiosResponse(response))); + return { + response, + }; + }; + + it('should list all consent sessions', async () => { + const { response } = setup(); + const result: ProviderConsentSessionResponse[] = await service.listConsentSessions('userId'); expect(result).toEqual(response); @@ -356,8 +488,12 @@ describe('HydraService', () => { }); describe('revokeConsentSession', () => { - it('should revoke all consent sessions', async () => { + const setup = () => { httpService.request.mockReturnValue(of(createAxiosResponse({}))); + }; + + it('should revoke all consent sessions', async () => { + setup(); await service.revokeConsentSession('userId', 'clientId'); @@ -375,7 +511,7 @@ describe('HydraService', () => { describe('Logout Flow', () => { describe('acceptLogoutRequest', () => { - it('should make http request', async () => { + const setup = () => { const responseMock: ProviderRedirectResponse = { redirect_to: 'redirect_mock' }; httpService.request.mockReturnValue(of(createAxiosResponse(responseMock))); const config: AxiosRequestConfig = { @@ -384,6 +520,15 @@ describe('HydraService', () => { headers: { 'X-Forwarded-Proto': 'https' }, }; + return { + responseMock, + config, + }; + }; + + it('should make http request', async () => { + const { responseMock, config } = setup(); + const response: ProviderRedirectResponse = await service.acceptLogoutRequest('challenge_mock'); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); @@ -394,12 +539,20 @@ describe('HydraService', () => { describe('Miscellaneous', () => { describe('introspectOAuth2Token', () => { - it('should return introspect', async () => { + const setup = () => { const response: IntrospectResponse = { active: true, }; httpService.request.mockReturnValue(of(createAxiosResponse(response))); + return { + response, + }; + }; + + it('should return introspect', async () => { + const { response } = setup(); + const result: IntrospectResponse = await service.introspectOAuth2Token('token', 'scope'); expect(result).toEqual(response); @@ -418,8 +571,12 @@ describe('HydraService', () => { }); describe('isInstanceAlive', () => { - it('should check if hydra is alive', async () => { + const setup = () => { httpService.request.mockReturnValue(of(createAxiosResponse(true))); + }; + + it('should check if hydra is alive', async () => { + setup(); const result: boolean = await service.isInstanceAlive(); @@ -459,25 +616,30 @@ describe('HydraService', () => { }); describe('getLoginRequest', () => { - it('should send login request', async () => { - // Arrange + const setup = () => { const requestConfig: AxiosRequestConfig = { method: 'GET', url: `${hydraUri}/oauth2/auth/requests/login?login_challenge=${challenge}`, }; httpService.request.mockReturnValue(of(createAxiosResponse(providerLoginResponse))); - // Act + return { + requestConfig, + }; + }; + + it('should send login request', async () => { + const { requestConfig } = setup(); + const response: ProviderLoginResponse = await service.getLoginRequest(challenge); - // Assert expect(response).toEqual(providerLoginResponse); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(requestConfig)); }); }); describe('acceptLoginRequest', () => { - it('should send accept login request', async () => { + const setup = () => { const body: AcceptLoginRequestBody = { subject: '', force_subject_identifier: '', @@ -494,6 +656,16 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should send accept login request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.acceptLoginRequest(challenge, body); expect(result.redirect_to).toEqual(expectedRedirectTo); @@ -502,8 +674,7 @@ describe('HydraService', () => { }); describe('rejectLoginRequest', () => { - it('should send reject login request', async () => { - // Arrange + const setup = () => { const body: RejectRequestBody = { error: 'error', }; @@ -517,10 +688,18 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); - // Act + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should send reject login request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.rejectLoginRequest(challenge, body); - // Assert expect(result.redirect_to).toEqual(expectedRedirectTo); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); diff --git a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts index f554a15abd3..3e2d389d643 100644 --- a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts +++ b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts @@ -1,21 +1,23 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; -import { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; +import { AxiosResponse, isAxiosError, Method, RawAxiosRequestHeaders } from 'axios'; import QueryString from 'qs'; -import { Observable, firstValueFrom } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { URL } from 'url'; import { AcceptConsentRequestBody, AcceptLoginRequestBody, IntrospectResponse, ProviderConsentResponse, + ProviderConsentSessionResponse, ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, RejectRequestBody, } from '../dto'; -import { ProviderConsentSessionResponse } from '../dto/response/consent-session.response'; +import { HydraOauthFailedLoggableException } from '../loggable'; import { OauthProviderService } from '../oauth-provider.service'; @Injectable() @@ -160,15 +162,26 @@ export class HydraAdapter extends OauthProviderService { data?: unknown, additionalHeaders: RawAxiosRequestHeaders = {} ): Promise { - const observable: Observable> = this.httpService.request({ - url, - method, - headers: { - 'X-Forwarded-Proto': 'https', - ...additionalHeaders, - }, - data, - }); + const observable: Observable> = this.httpService + .request({ + url, + method, + headers: { + 'X-Forwarded-Proto': 'https', + ...additionalHeaders, + }, + data, + }) + .pipe( + catchError((error: unknown) => { + if (isAxiosError(error)) { + throw new HydraOauthFailedLoggableException(error); + } else { + throw error; + } + }) + ); + const response: AxiosResponse = await firstValueFrom(observable); return response.data; } diff --git a/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.spec.ts b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.spec.ts new file mode 100644 index 00000000000..a78b365d126 --- /dev/null +++ b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.spec.ts @@ -0,0 +1,35 @@ +import { axiosErrorFactory } from '@shared/testing/factory'; +import { AxiosError } from 'axios'; +import { HydraOauthFailedLoggableException } from './hydra-oauth-failed-loggable-exception'; + +describe(HydraOauthFailedLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const error = { + error: 'invalid_request', + }; + const axiosError: AxiosError = axiosErrorFactory.withError(error).build({ stack: 'someStack' }); + + const exception = new HydraOauthFailedLoggableException(axiosError); + + return { + exception, + axiosError, + error, + }; + }; + + it('should return the correct log message', () => { + const { exception, axiosError, error } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'HYDRA_OAUTH_FAILED', + message: axiosError.message, + stack: axiosError.stack, + data: JSON.stringify(error), + }); + }); + }); +}); diff --git a/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.ts b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.ts new file mode 100644 index 00000000000..c92dd3c7fff --- /dev/null +++ b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-failed-loggable-exception.ts @@ -0,0 +1,8 @@ +import { AxiosErrorLoggable } from '@src/core/error/loggable'; +import { AxiosError } from 'axios'; + +export class HydraOauthFailedLoggableException extends AxiosErrorLoggable { + constructor(error: AxiosError) { + super(error, 'HYDRA_OAUTH_FAILED'); + } +} diff --git a/apps/server/src/infra/oauth-provider/loggable/index.ts b/apps/server/src/infra/oauth-provider/loggable/index.ts new file mode 100644 index 00000000000..677fe4f84e6 --- /dev/null +++ b/apps/server/src/infra/oauth-provider/loggable/index.ts @@ -0,0 +1 @@ +export * from './hydra-oauth-failed-loggable-exception'; diff --git a/apps/server/src/modules/oauth/loggable/index.ts b/apps/server/src/modules/oauth/loggable/index.ts index b4e63107161..4c35983a4ca 100644 --- a/apps/server/src/modules/oauth/loggable/index.ts +++ b/apps/server/src/modules/oauth/loggable/index.ts @@ -1,3 +1,4 @@ export * from './oauth-sso.error'; export * from './sso-error-code.enum'; export * from './user-not-found-after-provisioning.loggable-exception'; +export * from './token-request-loggable-exception'; diff --git a/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts b/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts index 35659b2778f..cc1486adcb7 100644 --- a/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts +++ b/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts @@ -1,6 +1,10 @@ import { InternalServerErrorException } from '@nestjs/common'; import { SSOErrorCode } from './sso-error-code.enum'; +/** + * @deprecated Please create a loggable instead. + * This will be removed with: https://ticketsystem.dbildungscloud.de/browse/N21-1483 + */ export class OAuthSSOError extends InternalServerErrorException { readonly message: string; diff --git a/apps/server/src/modules/oauth/loggable/token-request-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/token-request-loggable-exception.spec.ts new file mode 100644 index 00000000000..6716175bdbe --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/token-request-loggable-exception.spec.ts @@ -0,0 +1,34 @@ +import { axiosErrorFactory } from '@shared/testing/factory'; +import { AxiosError } from 'axios'; +import { TokenRequestLoggableException } from './token-request-loggable-exception'; + +describe(TokenRequestLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const error = { + error: 'invalid_request', + }; + const axiosError: AxiosError = axiosErrorFactory.withError(error).build(); + const exception = new TokenRequestLoggableException(axiosError); + + return { + axiosError, + exception, + error, + }; + }; + + it('should return the correct log message', () => { + const { axiosError, exception, error } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toStrictEqual({ + type: 'OAUTH_TOKEN_REQUEST_ERROR', + message: axiosError.message, + data: JSON.stringify(error), + stack: axiosError.stack, + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/token-request-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/token-request-loggable-exception.ts new file mode 100644 index 00000000000..fd852186829 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/token-request-loggable-exception.ts @@ -0,0 +1,8 @@ +import { AxiosErrorLoggable } from '@src/core/error/loggable'; +import { AxiosError } from 'axios'; + +export class TokenRequestLoggableException extends AxiosErrorLoggable { + constructor(error: AxiosError) { + super(error, 'OAUTH_TOKEN_REQUEST_ERROR'); + } +} diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts index 12c0a381d8b..af03a6fdda2 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts @@ -2,9 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { axiosResponseFactory } from '@shared/testing'; +import { axiosErrorFactory } from '@shared/testing/factory'; +import { AxiosError } from 'axios'; import { of, throwError } from 'rxjs'; import { OAuthGrantType } from '../interface/oauth-grant-type.enum'; -import { OAuthSSOError } from '../loggable'; +import { TokenRequestLoggableException } from '../loggable'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; @@ -93,12 +95,65 @@ describe('OauthAdapterServive', () => { }); describe('when no token got returned', () => { + const setup = () => { + const error = new Error('unknown error'); + httpService.post.mockReturnValueOnce(throwError(() => error)); + + return { + error, + }; + }; + it('should throw an error', async () => { - httpService.post.mockReturnValueOnce(throwError(() => 'error')); + const { error } = setup(); const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); - await expect(resp).rejects.toEqual(new OAuthSSOError('Requesting token failed.', 'sso_auth_code_step')); + await expect(resp).rejects.toEqual(error); + }); + }); + + describe('when error got returned', () => { + describe('when error is a unknown error', () => { + const setup = () => { + const error = new Error('unknown error'); + httpService.post.mockReturnValueOnce(throwError(() => error)); + + return { + error, + }; + }; + + it('should throw the default sso error', async () => { + const { error } = setup(); + + const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + + await expect(resp).rejects.toEqual(error); + }); + }); + + describe('when error is a axios error', () => { + const setup = () => { + const error = { + error: 'invalid_request', + }; + const axiosError: AxiosError = axiosErrorFactory.withError(error).build(); + + httpService.post.mockReturnValueOnce(throwError(() => axiosError)); + + return { + axiosError, + }; + }; + + it('should throw an error', async () => { + const { axiosError } = setup(); + + const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + + await expect(resp).rejects.toEqual(new TokenRequestLoggableException(axiosError)); + }); }); }); }); diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts index 6b008b610cf..4ab048b84c4 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts @@ -1,10 +1,10 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common/decorators'; -import { AxiosResponse } from 'axios'; +import { AxiosResponse, isAxiosError } from 'axios'; import JwksRsa from 'jwks-rsa'; import QueryString from 'qs'; import { lastValueFrom, Observable } from 'rxjs'; -import { OAuthSSOError } from '../loggable'; +import { TokenRequestLoggableException } from '../loggable'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; @Injectable() @@ -40,8 +40,11 @@ export class OauthAdapterService { let responseToken: AxiosResponse; try { responseToken = await lastValueFrom(observable); - } catch (error) { - throw new OAuthSSOError('Requesting token failed.', 'sso_auth_code_step'); + } catch (error: unknown) { + if (isAxiosError(error)) { + throw new TokenRequestLoggableException(error); + } + throw error; } return responseToken.data; diff --git a/apps/server/src/shared/testing/factory/axios-error.factory.ts b/apps/server/src/shared/testing/factory/axios-error.factory.ts new file mode 100644 index 00000000000..089179dafef --- /dev/null +++ b/apps/server/src/shared/testing/factory/axios-error.factory.ts @@ -0,0 +1,28 @@ +import { HttpStatus } from '@nestjs/common'; +import { axiosResponseFactory } from '@shared/testing'; +import { AxiosError, AxiosHeaders } from 'axios'; +import { Factory } from 'fishery'; + +class AxiosErrorFactory extends Factory { + withError(error: unknown): this { + return this.params({ + response: axiosResponseFactory.build({ status: HttpStatus.BAD_REQUEST, data: error }), + }); + } +} + +export const axiosErrorFactory = AxiosErrorFactory.define(() => { + return { + status: HttpStatus.BAD_REQUEST, + config: { headers: new AxiosHeaders() }, + isAxiosError: true, + code: HttpStatus.BAD_REQUEST.toString(), + message: 'Bad Request', + name: 'BadRequest', + response: axiosResponseFactory.build({ status: HttpStatus.BAD_REQUEST }), + stack: 'mockStack', + toJSON: () => { + return { someJson: 'someJson' }; + }, + }; +}); diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index bd8d0913a72..54fac672098 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -39,3 +39,4 @@ export * from './user.do.factory'; export * from './user.factory'; export * from './legacy-file-entity-mock.factory'; export * from './jwt.test.factory'; +export * from './axios-error.factory'; From b11b022db6c28abfa6900b45943b4760d856d826 Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Mon, 13 Nov 2023 11:13:18 +0100 Subject: [PATCH 17/23] N21-1374 removes old login flow (#4541) --- .../controller/api-test/oauth-sso.api.spec.ts | 582 ----------- .../controller/dto/authorization.params.ts | 3 - .../controller/oauth-sso.controller.spec.ts | 5 - .../oauth/controller/oauth-sso.controller.ts | 174 +--- .../oauth/mapper/oauth-login-state.mapper.ts | 9 - .../src/modules/oauth/oauth-api.module.ts | 10 +- .../oauth/service/oauth.service.spec.ts | 92 +- .../modules/oauth/service/oauth.service.ts | 25 - .../oauth/uc/dto/oauth-login-state.dto.ts | 21 - .../modules/oauth/uc/hydra-oauth.uc.spec.ts | 2 +- .../src/modules/oauth/uc/hydra-oauth.uc.ts | 7 +- apps/server/src/modules/oauth/uc/index.ts | 1 - .../src/modules/oauth/uc/oauth.uc.spec.ts | 923 ------------------ apps/server/src/modules/oauth/uc/oauth.uc.ts | 149 --- config/default.schema.json | 5 - src/services/config/publicAppConfigService.js | 1 - 16 files changed, 24 insertions(+), 1985 deletions(-) delete mode 100644 apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts delete mode 100644 apps/server/src/modules/oauth/mapper/oauth-login-state.mapper.ts delete mode 100644 apps/server/src/modules/oauth/uc/dto/oauth-login-state.dto.ts delete mode 100644 apps/server/src/modules/oauth/uc/oauth.uc.spec.ts delete mode 100644 apps/server/src/modules/oauth/uc/oauth.uc.ts diff --git a/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts b/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts deleted file mode 100644 index a259c405cfb..00000000000 --- a/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts +++ /dev/null @@ -1,582 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Account, EntityId, SchoolEntity, SystemEntity, User } from '@shared/domain'; -import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { KeycloakAdministrationService } from '@infra/identity-management/keycloak-administration/service/keycloak-administration.service'; -import { - accountFactory, - cleanupCollections, - mapUserToCurrentUser, - schoolFactory, - systemFactory, - userFactory, -} from '@shared/testing'; -import { JwtTestFactory } from '@shared/testing/factory/jwt.test.factory'; -import { userLoginMigrationFactory } from '@shared/testing/factory/user-login-migration.factory'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { SanisResponse, SanisRole } from '@modules/provisioning/strategy/sanis/response'; -import { ServerTestModule } from '@modules/server'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { UUID } from 'bson'; -import { Request } from 'express'; -import request, { Response } from 'supertest'; -import { SSOAuthenticationError } from '../../interface/sso-authentication-error.enum'; -import { OauthTokenResponse } from '../../service/dto'; -import { AuthorizationParams, SSOLoginQuery } from '../dto'; - -jest.mock('jwks-rsa', () => () => { - return { - getKeys: jest.fn(), - getSigningKey: jest.fn().mockResolvedValue({ - kid: 'kid', - alg: 'RS256', - getPublicKey: jest.fn().mockReturnValue(JwtTestFactory.getPublicKey()), - rsaPublicKey: JwtTestFactory.getPublicKey(), - }), - getSigningKeys: jest.fn(), - }; -}); - -describe('OAuth SSO Controller (API)', () => { - let app: INestApplication; - let em: EntityManager; - let currentUser: ICurrentUser; - let axiosMock: MockAdapter; - - const sessionCookieName: string = Configuration.get('SESSION__NAME') as string; - beforeAll(async () => { - Configuration.set('PUBLIC_BACKEND_URL', 'http://localhost:3030/api'); - const schulcloudJwt: string = JwtTestFactory.createJwt(); - - const moduleRef: TestingModule = await Test.createTestingModule({ - imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - req.headers.authorization = schulcloudJwt; - return true; - }, - }) - .compile(); - - axiosMock = new MockAdapter(axios); - app = moduleRef.createNestApplication(); - await app.init(); - em = app.get(EntityManager); - const kcAdminService = app.get(KeycloakAdministrationService); - - axiosMock.onGet(kcAdminService.getWellKnownUrl()).reply(200, { - issuer: 'issuer', - token_endpoint: 'token_endpoint', - authorization_endpoint: 'authorization_endpoint', - end_session_endpoint: 'end_session_endpoint', - jwks_uri: 'jwks_uri', - }); - }); - - afterAll(async () => { - await app.close(); - }); - - afterEach(async () => { - await cleanupCollections(em); - }); - - const setupSessionState = async (systemId: EntityId, migration: boolean) => { - const query: SSOLoginQuery = { - migration, - }; - - const response: Response = await request(app.getHttpServer()) - .get(`/sso/login/${systemId}`) - .query(query) - .expect(302) - .expect('set-cookie', new RegExp(`^${sessionCookieName}`)); - - const cookies: string[] = response.get('Set-Cookie'); - const redirect: string = response.get('Location'); - const matchState: RegExpMatchArray | null = redirect.match(/(?<=state=)([^&]+)/); - const state = matchState ? matchState[0] : ''; - - return { - cookies, - state, - }; - }; - - const setup = async () => { - const externalUserId = 'externalUserId'; - const system: SystemEntity = systemFactory.withOauthConfig().buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system] }); - const user: User = userFactory.buildWithId({ externalId: externalUserId, school }); - const account: Account = accountFactory.buildWithId({ systemId: system.id, userId: user.id }); - - await em.persistAndFlush([system, user, school, account]); - em.clear(); - - const query: AuthorizationParams = new AuthorizationParams(); - query.code = 'code'; - query.state = 'state'; - - return { - system, - user, - externalUserId, - school, - query, - }; - }; - - describe('[GET] sso/login/:systemId', () => { - describe('when no error occurs', () => { - it('should redirect to the authentication url and set a session cookie', async () => { - const { system } = await setup(); - - await request(app.getHttpServer()) - .get(`/sso/login/${system.id}`) - .expect(302) - .expect('set-cookie', new RegExp(`^${sessionCookieName}`)) - .expect( - 'Location', - /^http:\/\/mock.de\/auth\?client_id=12345&redirect_uri=http%3A%2F%2Flocalhost%3A3030%2Fapi%2Fv3%2Fsso%2Foauth&response_type=code&scope=openid\+uuid&state=\w*/ - ); - }); - }); - - describe('when an error occurs', () => { - it('should redirect to the login page', async () => { - const unknownSystemId: string = new ObjectId().toHexString(); - const clientUrl: string = Configuration.get('HOST') as string; - - await request(app.getHttpServer()) - .get(`/sso/login/${unknownSystemId}`) - .expect(302) - .expect('Location', `${clientUrl}/login?error=sso_login_failed`); - }); - }); - }); - - describe('[GET] sso/oauth', () => { - describe('when the session has no oauthLoginState', () => { - it('should return 401 Unauthorized', async () => { - await setup(); - const query: AuthorizationParams = new AuthorizationParams(); - query.code = 'code'; - query.state = 'state'; - - await request(app.getHttpServer()).get(`/sso/oauth`).query(query).expect(401); - }); - }); - - describe('when the session and the request have a different state', () => { - it('should return 401 Unauthorized', async () => { - const { system } = await setup(); - const { cookies } = await setupSessionState(system.id, false); - const query: AuthorizationParams = new AuthorizationParams(); - query.code = 'code'; - query.state = 'wrongState'; - - await request(app.getHttpServer()).get(`/sso/oauth`).set('Cookie', cookies).query(query).expect(401); - }); - }); - - describe('when code and state are valid', () => { - it('should set a jwt and redirect', async () => { - const { system, externalUserId, query } = await setup(); - const { state, cookies } = await setupSessionState(system.id, false); - const baseUrl: string = Configuration.get('HOST') as string; - query.code = 'code'; - query.state = state; - - const idToken: string = JwtTestFactory.createJwt({ - sub: 'testUser', - iss: system.oauthConfig?.issuer, - aud: system.oauthConfig?.clientId, - // For OIDC provisioning strategy - external_sub: externalUserId, - }); - - axiosMock.onPost(system.oauthConfig?.tokenEndpoint).reply(200, { - id_token: idToken, - refresh_token: 'refreshToken', - access_token: 'accessToken', - }); - - await request(app.getHttpServer()) - .get(`/sso/oauth`) - .set('Cookie', cookies) - .query(query) - .expect(302) - .expect('Location', `${baseUrl}/dashboard`) - .expect( - (res: Response) => res.get('Set-Cookie').filter((value: string) => value.startsWith('jwt')).length === 1 - ); - }); - }); - - describe('when an error occurs during the login process', () => { - it('should redirect to the login page', async () => { - const { system, query } = await setup(); - const { state, cookies } = await setupSessionState(system.id, false); - const clientUrl: string = Configuration.get('HOST') as string; - query.error = SSOAuthenticationError.ACCESS_DENIED; - query.state = state; - - await request(app.getHttpServer()) - .get(`/sso/oauth`) - .set('Cookie', cookies) - .query(query) - .expect(302) - .expect( - 'Location', - `${clientUrl}/login?error=access_denied&provider=${system.oauthConfig?.provider as string}` - ); - }); - }); - - describe('when a faulty query is passed', () => { - it('should redirect to the login page with an error', async () => { - const { system, query } = await setup(); - const { state, cookies } = await setupSessionState(system.id, false); - const clientUrl: string = Configuration.get('HOST') as string; - query.state = state; - query.code = undefined; - - await request(app.getHttpServer()) - .get(`/sso/oauth`) - .set('Cookie', cookies) - .query(query) - .expect(302) - .expect( - 'Location', - `${clientUrl}/login?error=sso_auth_code_step&provider=${system.oauthConfig?.provider as string}` - ); - }); - }); - }); - - describe('[GET] sso/oauth/migration', () => { - const mockPostOauthTokenEndpoint = ( - idToken: string, - targetSystem: SystemEntity, - targetUserId: string, - schoolExternalId: string, - officialSchoolNumber: string - ) => { - axiosMock - .onPost(targetSystem.oauthConfig?.tokenEndpoint) - .replyOnce(200, { - id_token: idToken, - refresh_token: 'refreshToken', - access_token: 'accessToken', - }) - .onGet(targetSystem.provisioningUrl) - .replyOnce(200, { - pid: targetUserId, - person: { - name: { - familienname: 'familienName', - vorname: 'vorname', - }, - geschlecht: 'weiblich', - lokalisierung: 'not necessary', - vertrauensstufe: 'not necessary', - }, - personenkontexte: [ - { - id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713').toString(), - rolle: SanisRole.LEHR, - organisation: { - id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713').toString(), - kennung: officialSchoolNumber, - name: 'schulName', - typ: 'not necessary', - }, - personenstatus: 'not necessary', - }, - ], - }); - }; - - describe('when the session has no oauthLoginState', () => { - it('should return 401 Unauthorized', async () => { - const { query } = await setup(); - - await request(app.getHttpServer()).get(`/sso/oauth/migration`).query(query).expect(401); - }); - }); - - describe('when the migration is successful', () => { - const setupMigration = async () => { - const { externalUserId, query } = await setup(); - - const targetSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }, new ObjectId().toHexString(), {}); - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.ISERV }, new ObjectId().toHexString(), {}); - - const sourceSchool: SchoolEntity = schoolFactory.buildWithId({ - systems: [sourceSystem], - officialSchoolNumber: '11111', - externalId: 'aef1f4fd-c323-466e-962b-a84354c0e713', - }); - const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ - school: sourceSchool, - targetSystem, - sourceSystem, - startedAt: new Date('2022-12-17T03:24:00'), - }); - - const targetSchoolExternalId = 'aef1f4fd-c323-466e-962b-a84354c0e714'; - - const sourceUser: User = userFactory.buildWithId({ externalId: externalUserId, school: sourceSchool }); - - const sourceUserAccount: Account = accountFactory.buildWithId({ - userId: sourceUser.id, - systemId: sourceSystem.id, - username: sourceUser.email, - }); - - await em.persistAndFlush([sourceSystem, targetSystem, sourceUser, sourceUserAccount, userLoginMigration]); - - const { state, cookies } = await setupSessionState(targetSystem.id, true); - query.code = 'code'; - query.state = state; - - return { - targetSystem, - targetSchoolExternalId, - sourceSystem, - sourceUser, - externalUserId, - query, - cookies, - }; - }; - - it('should redirect to the success page', async () => { - const { query, sourceUser, targetSystem, externalUserId, cookies, sourceSystem, targetSchoolExternalId } = - await setupMigration(); - currentUser = mapUserToCurrentUser(sourceUser, undefined, sourceSystem.id); - const baseUrl: string = Configuration.get('HOST') as string; - - const idToken: string = JwtTestFactory.createJwt({ - sub: 'testUser', - iss: targetSystem.oauthConfig?.issuer, - aud: targetSystem.oauthConfig?.clientId, - external_sub: externalUserId, - }); - - mockPostOauthTokenEndpoint(idToken, targetSystem, currentUser.userId, targetSchoolExternalId, 'NI_11111'); - - await request(app.getHttpServer()) - .get(`/sso/oauth/migration`) - .set('Cookie', cookies) - .query(query) - .expect(302) - .expect( - 'Location', - `${baseUrl}/migration/success?sourceSystem=${ - currentUser.systemId ? currentUser.systemId : '' - }&targetSystem=${targetSystem.id}` - ); - }); - }); - - describe('when currentUser has no systemId', () => { - const setupMigration = async () => { - const { externalUserId, query } = await setup(); - - const targetSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }, new ObjectId().toHexString(), {}); - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.ISERV }, new ObjectId().toHexString(), {}); - - const sourceSchool: SchoolEntity = schoolFactory.buildWithId({ - systems: [sourceSystem], - officialSchoolNumber: '11110', - externalId: 'aef1f4fd-c323-466e-962b-a84354c0e713', - }); - - const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ - school: sourceSchool, - targetSystem, - sourceSystem, - startedAt: new Date('2022-12-17T03:24:00'), - }); - - const sourceUser: User = userFactory.buildWithId({ externalId: externalUserId, school: sourceSchool }); - - await em.persistAndFlush([targetSystem, sourceUser, userLoginMigration]); - - const { state, cookies } = await setupSessionState(targetSystem.id, true); - query.code = 'code'; - query.state = state; - - return { - sourceUser, - query, - cookies, - }; - }; - - it('should throw UnprocessableEntityException', async () => { - const { sourceUser, query, cookies } = await setupMigration(); - currentUser = mapUserToCurrentUser(sourceUser, undefined, undefined); - query.error = SSOAuthenticationError.INVALID_REQUEST; - - await request(app.getHttpServer()).get(`/sso/oauth/migration`).set('Cookie', cookies).query(query).expect(422); - }); - }); - - describe('when invalid request', () => { - const setupMigration = async () => { - const { externalUserId, query } = await setup(); - - const targetSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }, new ObjectId().toHexString(), {}); - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.ISERV }, new ObjectId().toHexString(), {}); - - const sourceSchool: SchoolEntity = schoolFactory.buildWithId({ - systems: [sourceSystem], - officialSchoolNumber: '11111', - externalId: 'aef1f4fd-c323-466e-962b-a84354c0e713', - }); - - const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ - school: sourceSchool, - targetSystem, - sourceSystem, - startedAt: new Date('2022-12-17T03:24:00'), - }); - - const sourceUser: User = userFactory.buildWithId({ externalId: externalUserId, school: sourceSchool }); - - await em.persistAndFlush([sourceSystem, targetSystem, sourceSchool, sourceUser, userLoginMigration]); - - const { state, cookies } = await setupSessionState(targetSystem.id, true); - query.code = 'code'; - query.state = state; - - return { - targetSystem, - sourceSystem, - sourceUser, - query, - cookies, - }; - }; - - it('should redirect to the general migration error page', async () => { - const { sourceUser, sourceSystem, query, cookies } = await setupMigration(); - currentUser = mapUserToCurrentUser(sourceUser, undefined, sourceSystem.id); - const baseUrl: string = Configuration.get('HOST') as string; - query.error = SSOAuthenticationError.INVALID_REQUEST; - - await request(app.getHttpServer()) - .get(`/sso/oauth/migration`) - .set('Cookie', cookies) - .query(query) - .expect(302) - .expect('Location', `${baseUrl}/migration/error`); - }); - }); - - describe('when schoolnumbers mismatch', () => { - const setupMigration = async () => { - const { externalUserId, query } = await setup(); - - const targetSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }, new ObjectId().toHexString(), {}); - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.ISERV }, new ObjectId().toHexString(), {}); - - const sourceSchool: SchoolEntity = schoolFactory.buildWithId({ - systems: [sourceSystem], - officialSchoolNumber: '11111', - externalId: 'aef1f4fd-c323-466e-962b-a84354c0e713', - }); - - const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ - school: sourceSchool, - targetSystem, - sourceSystem, - startedAt: new Date('2022-12-17T03:24:00'), - }); - - const targetSchool: SchoolEntity = schoolFactory.buildWithId({ - systems: [targetSystem], - officialSchoolNumber: '22222', - externalId: 'aef1f4fd-c323-466e-962b-a84354c0e713', - }); - - const sourceUser: User = userFactory.buildWithId({ externalId: externalUserId, school: sourceSchool }); - - const targetUser: User = userFactory.buildWithId({ - externalId: 'differentExternalUserId', - school: targetSchool, - }); - - await em.persistAndFlush([sourceSystem, targetSystem, sourceUser, targetUser, userLoginMigration]); - - const { state, cookies } = await setupSessionState(targetSystem.id, true); - query.code = 'code'; - query.state = state; - - return { - targetSystem, - sourceSystem, - sourceUser, - targetUser, - targetSchoolExternalId: targetSchool.externalId as string, - query, - cookies, - }; - }; - - it('should redirect to the login page with an schoolnumber mismatch error', async () => { - const { targetSystem, sourceUser, targetUser, sourceSystem, targetSchoolExternalId, query, cookies } = - await setupMigration(); - currentUser = mapUserToCurrentUser(sourceUser, undefined, sourceSystem.id); - const baseUrl: string = Configuration.get('HOST') as string; - - const idToken: string = JwtTestFactory.createJwt({ - sub: 'differentExternalUserId', - iss: targetSystem.oauthConfig?.issuer, - aud: targetSystem.oauthConfig?.clientId, - external_sub: 'differentExternalUserId', - }); - - mockPostOauthTokenEndpoint(idToken, targetSystem, targetUser.id, targetSchoolExternalId, 'NI_22222'); - - await request(app.getHttpServer()) - .get(`/sso/oauth/migration`) - .set('Cookie', cookies) - .query(query) - .expect(302) - .expect('Location', `${baseUrl}/migration/error?sourceSchoolNumber=11111&targetSchoolNumber=22222`); - }); - }); - - afterAll(() => { - axiosMock.restore(); - }); - }); -}); diff --git a/apps/server/src/modules/oauth/controller/dto/authorization.params.ts b/apps/server/src/modules/oauth/controller/dto/authorization.params.ts index 1a20985ce43..af76d0799e4 100644 --- a/apps/server/src/modules/oauth/controller/dto/authorization.params.ts +++ b/apps/server/src/modules/oauth/controller/dto/authorization.params.ts @@ -1,9 +1,6 @@ import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { SSOAuthenticationError } from '../../interface/sso-authentication-error.enum'; -/** - * @deprecated - */ export class AuthorizationParams { @IsOptional() @IsString() diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts index 3d1a470e227..c42eeb22e24 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts @@ -8,7 +8,6 @@ import { HydraOauthUc } from '@modules/oauth/uc/hydra-oauth.uc'; import { Request } from 'express'; import { OauthSSOController } from './oauth-sso.controller'; import { StatelessAuthorizationParams } from './dto/stateless-authorization.params'; -import { OauthUc } from '../uc'; describe('OAuthController', () => { let module: TestingModule; @@ -52,10 +51,6 @@ describe('OAuthController', () => { provide: LegacyLogger, useValue: createMock(), }, - { - provide: OauthUc, - useValue: createMock(), - }, { provide: HydraOauthUc, useValue: createMock(), diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts index 5ff7e7cae02..61ed319d1cd 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts @@ -1,150 +1,18 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { - Controller, - Get, - InternalServerErrorException, - Param, - Query, - Req, - Res, - Session, - UnauthorizedException, - UnprocessableEntityException, -} from '@nestjs/common'; -import { ApiOkResponse, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { ISession } from '@shared/domain/types/session'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { Controller, Get, Param, Query, Req, UnauthorizedException } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser, Authenticate, CurrentUser, JWT } from '@modules/authentication'; -import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error'; -import { MigrationDto } from '@modules/user-login-migration/service/dto'; -import { CookieOptions, Request, Response } from 'express'; -import { HydraOauthUc } from '../uc/hydra-oauth.uc'; -import { UserMigrationResponse } from './dto/user-migration.response'; -import { OAuthSSOError } from '../loggable/oauth-sso.error'; +import { Request } from 'express'; import { OAuthTokenDto } from '../interface'; -import { OauthLoginStateMapper } from '../mapper/oauth-login-state.mapper'; -import { UserMigrationMapper } from '../mapper/user-migration.mapper'; -import { OAuthProcessDto } from '../service/dto'; -import { OauthUc } from '../uc'; -import { OauthLoginStateDto } from '../uc/dto/oauth-login-state.dto'; -import { AuthorizationParams, SSOLoginQuery, SystemIdParams } from './dto'; +import { HydraOauthUc } from '../uc'; +import { AuthorizationParams } from './dto'; import { StatelessAuthorizationParams } from './dto/stateless-authorization.params'; @ApiTags('SSO') @Controller('sso') export class OauthSSOController { - private readonly clientUrl: string; - - constructor( - private readonly oauthUc: OauthUc, - private readonly hydraUc: HydraOauthUc, - private readonly logger: LegacyLogger - ) { + constructor(private readonly hydraUc: HydraOauthUc, private readonly logger: LegacyLogger) { this.logger.setContext(OauthSSOController.name); - this.clientUrl = Configuration.get('HOST') as string; - } - - private errorHandler(error: unknown, session: ISession, res: Response, provider?: string) { - this.logger.error(error); - const ssoError: OAuthSSOError = error instanceof OAuthSSOError ? error : new OAuthSSOError(); - - session.destroy((err) => { - this.logger.log(err); - }); - - const errorRedirect: URL = new URL('/login', this.clientUrl); - errorRedirect.searchParams.append('error', ssoError.errorcode); - - if (provider) { - errorRedirect.searchParams.append('provider', provider); - } - - res.redirect(errorRedirect.toString()); - } - - private migrationErrorHandler(error: unknown, session: ISession, res: Response) { - const migrationError: OAuthMigrationError = - error instanceof OAuthMigrationError ? error : new OAuthMigrationError(); - - session.destroy((err) => { - this.logger.log(err); - }); - - const errorRedirect: URL = new URL('/migration/error', this.clientUrl); - - if (migrationError.officialSchoolNumberFromSource && migrationError.officialSchoolNumberFromTarget) { - errorRedirect.searchParams.append('sourceSchoolNumber', migrationError.officialSchoolNumberFromSource); - errorRedirect.searchParams.append('targetSchoolNumber', migrationError.officialSchoolNumberFromTarget); - } - - res.redirect(errorRedirect.toString()); - } - - private sessionHandler(session: ISession, query: AuthorizationParams): OauthLoginStateDto { - if (!session.oauthLoginState) { - throw new UnauthorizedException('Oauth session not found'); - } - - const oauthLoginState: OauthLoginStateDto = OauthLoginStateMapper.mapSessionToDto(session); - - if (oauthLoginState.state !== query.state) { - throw new UnauthorizedException(`Invalid state. Got: ${query.state} Expected: ${oauthLoginState.state}`); - } - - return oauthLoginState; - } - - @Get('login/:systemId') - async getAuthenticationUrl( - @Session() session: ISession, - @Res() res: Response, - @Param() params: SystemIdParams, - @Query() query: SSOLoginQuery - ): Promise { - try { - const redirect: string = await this.oauthUc.startOauthLogin( - session, - params.systemId, - query.migration || false, - query.postLoginRedirect - ); - - res.redirect(redirect); - } catch (error) { - this.errorHandler(error, session, res); - } - } - - @Get('oauth') - async startOauthAuthorizationCodeFlow( - @Session() session: ISession, - @Res() res: Response, - @Query() query: AuthorizationParams - ): Promise { - const oauthLoginState: OauthLoginStateDto = this.sessionHandler(session, query); - - try { - const oauthProcessDto: OAuthProcessDto = await this.oauthUc.processOAuthLogin( - oauthLoginState, - query.code, - query.error - ); - - if (oauthProcessDto.jwt) { - const cookieDefaultOptions: CookieOptions = { - httpOnly: Configuration.get('COOKIE__HTTP_ONLY') as boolean, - sameSite: Configuration.get('COOKIE__SAME_SITE') as 'lax' | 'strict' | 'none', - secure: Configuration.get('COOKIE__SECURE') as boolean, - expires: new Date(Date.now() + (Configuration.get('COOKIE__EXPIRES_SECONDS') as number)), - }; - - res.cookie('jwt', oauthProcessDto.jwt, cookieDefaultOptions); - } - - res.redirect(oauthProcessDto.redirect); - } catch (error) { - this.errorHandler(error, session, res, oauthLoginState.provider); - } } @Get('hydra/:oauthClientId') @@ -166,7 +34,7 @@ export class OauthSSOController { ): Promise { let jwt: string; const authHeader: string | undefined = req.headers.authorization; - if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) { + if (authHeader?.toLowerCase()?.startsWith('bearer ')) { [, jwt] = authHeader.split(' '); } else { throw new UnauthorizedException( @@ -175,30 +43,4 @@ export class OauthSSOController { } return this.hydraUc.requestAuthCode(currentUser.userId, jwt, oauthClientId); } - - @Get('oauth/migration') - @Authenticate('jwt') - @ApiOkResponse({ description: 'The User has been succesfully migrated.' }) - @ApiResponse({ type: InternalServerErrorException, description: 'The migration of the User was not possible. ' }) - async migrateUser( - @JWT() jwt: string, - @Session() session: ISession, - @CurrentUser() currentUser: ICurrentUser, - @Query() query: AuthorizationParams, - @Res() res: Response - ): Promise { - const oauthLoginState: OauthLoginStateDto = this.sessionHandler(session, query); - - if (!currentUser.systemId) { - throw new UnprocessableEntityException('Current user does not have a system.'); - } - - try { - const migration: MigrationDto = await this.oauthUc.migrate(jwt, currentUser.userId, query, oauthLoginState); - const response: UserMigrationResponse = UserMigrationMapper.mapDtoToResponse(migration); - res.redirect(response.redirect); - } catch (error) { - this.migrationErrorHandler(error, session, res); - } - } } diff --git a/apps/server/src/modules/oauth/mapper/oauth-login-state.mapper.ts b/apps/server/src/modules/oauth/mapper/oauth-login-state.mapper.ts deleted file mode 100644 index 67c1ae8a6ef..00000000000 --- a/apps/server/src/modules/oauth/mapper/oauth-login-state.mapper.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ISession } from '@shared/domain/types/session'; -import { OauthLoginStateDto } from '../uc/dto/oauth-login-state.dto'; - -export class OauthLoginStateMapper { - static mapSessionToDto(session: ISession): OauthLoginStateDto { - const dto = new OauthLoginStateDto(session.oauthLoginState as OauthLoginStateDto); - return dto; - } -} diff --git a/apps/server/src/modules/oauth/oauth-api.module.ts b/apps/server/src/modules/oauth/oauth-api.module.ts index 98e62d87eca..2efacf66adf 100644 --- a/apps/server/src/modules/oauth/oauth-api.module.ts +++ b/apps/server/src/modules/oauth/oauth-api.module.ts @@ -1,15 +1,15 @@ -import { Module } from '@nestjs/common'; -import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '@modules/authentication/authentication.module'; import { AuthorizationModule } from '@modules/authorization'; -import { ProvisioningModule } from '@modules/provisioning'; import { LegacySchoolModule } from '@modules/legacy-school'; +import { ProvisioningModule } from '@modules/provisioning'; import { SystemModule } from '@modules/system'; import { UserModule } from '@modules/user'; import { UserLoginMigrationModule } from '@modules/user-login-migration'; +import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; import { OauthSSOController } from './controller/oauth-sso.controller'; import { OauthModule } from './oauth.module'; -import { HydraOauthUc, OauthUc } from './uc'; +import { HydraOauthUc } from './uc'; @Module({ imports: [ @@ -24,6 +24,6 @@ import { HydraOauthUc, OauthUc } from './uc'; LoggerModule, ], controllers: [OauthSSOController], - providers: [OauthUc, HydraOauthUc], + providers: [HydraOauthUc], }) export class OauthApiModule {} diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 9c4b45582df..1ea2fe5ce07 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -1,23 +1,23 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; -import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity } from '@shared/domain'; -import { UserDO } from '@shared/domain/domainobject/user.do'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; -import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; +import { LegacySchoolService } from '@modules/legacy-school'; import { ProvisioningDto, ProvisioningService } from '@modules/provisioning'; import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; -import { LegacySchoolService } from '@modules/legacy-school'; import { OauthConfigDto } from '@modules/system/service'; import { SystemDto } from '@modules/system/service/dto/system.dto'; import { SystemService } from '@modules/system/service/system.service'; import { UserService } from '@modules/user'; import { MigrationCheckService, UserMigrationService } from '@modules/user-login-migration'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity } from '@shared/domain'; +import { UserDO } from '@shared/domain/domainobject/user.do'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; +import { LegacyLogger } from '@src/core/logger'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { OAuthTokenDto } from '../interface'; +import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; import { OAuthService } from './oauth.service'; @@ -560,80 +560,4 @@ describe('OAuthService', () => { }); }); }); - - describe('getAuthenticationUrl is called', () => { - describe('when a normal authentication url is requested', () => { - it('should return a authentication url', () => { - const oauthConfig: OauthConfig = new OauthConfig({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - redirectUri: 'http://mockhost:3030/api/v3/sso/oauth/testsystemId', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'http://mock.de/auth', - provider: 'mock_type', - logoutEndpoint: 'http://mock.de/logout', - issuer: 'mock_issuer', - jwksEndpoint: 'http://mock.de/jwks', - }); - - const result: string = service.getAuthenticationUrl(oauthConfig, 'state', false); - - expect(result).toEqual( - 'http://mock.de/auth?client_id=12345&redirect_uri=https%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Foauth&response_type=code&scope=openid+uuid&state=state' - ); - }); - }); - - describe('when a migration authentication url is requested', () => { - it('should return a authentication url', () => { - const oauthConfig: OauthConfig = new OauthConfig({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - redirectUri: 'http://mockhost.de/api/v3/sso/oauth/testsystemId', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'http://mock.de/auth', - provider: 'mock_type', - logoutEndpoint: 'http://mock.de/logout', - issuer: 'mock_issuer', - jwksEndpoint: 'http://mock.de/jwks', - }); - - const result: string = service.getAuthenticationUrl(oauthConfig, 'state', true); - - expect(result).toEqual( - 'http://mock.de/auth?client_id=12345&redirect_uri=https%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Foauth%2Fmigration&response_type=code&scope=openid+uuid&state=state' - ); - }); - - it('should return add an idp hint if existing authentication url', () => { - const oauthConfig: OauthConfig = new OauthConfig({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - redirectUri: 'http://mockhost.de/api/v3/sso/oauth/testsystemId', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'http://mock.de/auth', - provider: 'mock_type', - logoutEndpoint: 'http://mock.de/logout', - issuer: 'mock_issuer', - jwksEndpoint: 'http://mock.de/jwks', - idpHint: 'TheIdpHint', - }); - - const result: string = service.getAuthenticationUrl(oauthConfig, 'state', true); - - expect(result).toEqual( - 'http://mock.de/auth?client_id=12345&redirect_uri=https%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Foauth%2Fmigration&response_type=code&scope=openid+uuid&state=state&kc_idp_hint=TheIdpHint' - ); - }); - }); - }); }); diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 190c962cd97..1f7935d919e 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -186,31 +186,6 @@ export class OAuthService { return redirect; } - getAuthenticationUrl(oauthConfig: OauthConfig, state: string, migration: boolean): string { - const redirectUri: string = this.getRedirectUri(migration); - - const authenticationUrl: URL = new URL(oauthConfig.authEndpoint); - authenticationUrl.searchParams.append('client_id', oauthConfig.clientId); - authenticationUrl.searchParams.append('redirect_uri', redirectUri); - authenticationUrl.searchParams.append('response_type', oauthConfig.responseType); - authenticationUrl.searchParams.append('scope', oauthConfig.scope); - authenticationUrl.searchParams.append('state', state); - if (oauthConfig.idpHint) { - authenticationUrl.searchParams.append('kc_idp_hint', oauthConfig.idpHint); - } - - return authenticationUrl.toString(); - } - - getRedirectUri(migration: boolean) { - const publicBackendUrl: string = Configuration.get('PUBLIC_BACKEND_URL') as string; - - const path: string = migration ? 'api/v3/sso/oauth/migration' : 'api/v3/sso/oauth'; - const redirectUri: URL = new URL(path, publicBackendUrl); - - return redirectUri.toString(); - } - private buildTokenRequestPayload( code: string, oauthConfig: OauthConfig, diff --git a/apps/server/src/modules/oauth/uc/dto/oauth-login-state.dto.ts b/apps/server/src/modules/oauth/uc/dto/oauth-login-state.dto.ts deleted file mode 100644 index 10d01b596d2..00000000000 --- a/apps/server/src/modules/oauth/uc/dto/oauth-login-state.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { EntityId } from '@shared/domain'; - -export class OauthLoginStateDto { - state: string; - - systemId: EntityId; - - provider: string; - - postLoginRedirect?: string; - - userLoginMigration: boolean; - - constructor(props: OauthLoginStateDto) { - this.state = props.state; - this.systemId = props.systemId; - this.postLoginRedirect = props.postLoginRedirect; - this.provider = props.provider; - this.userLoginMigration = props.userLoginMigration; - } -} diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts index 3d42b0e977f..339bc6c09d9 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts @@ -13,7 +13,7 @@ import { AxiosResponse } from 'axios'; import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; -import { OAuthSSOError } from '../loggable/oauth-sso.error'; +import { OAuthSSOError } from '../loggable'; import { OAuthTokenDto } from '../interface'; class HydraOauthUcSpec extends HydraOauthUc { diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts index 905cd3c8802..2c461e6db4d 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts @@ -1,12 +1,11 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { OauthConfig } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { AuthorizationParams } from '../controller/dto'; -import { OAuthSSOError } from '../loggable/oauth-sso.error'; import { OAuthTokenDto } from '../interface'; +import { OAuthSSOError } from '../loggable'; import { HydraSsoService } from '../service/hydra.service'; import { OAuthService } from '../service/oauth.service'; @@ -22,8 +21,6 @@ export class HydraOauthUc { private readonly MAX_REDIRECTS: number = 10; - private readonly HYDRA_PUBLIC_URI: string = Configuration.get('HYDRA_PUBLIC_URI') as string; - async getOauthToken(oauthClientId: string, code?: string, error?: string): Promise { if (error || !code) { throw new OAuthSSOError( diff --git a/apps/server/src/modules/oauth/uc/index.ts b/apps/server/src/modules/oauth/uc/index.ts index 32e4dce0f74..e1a569e5f88 100644 --- a/apps/server/src/modules/oauth/uc/index.ts +++ b/apps/server/src/modules/oauth/uc/index.ts @@ -1,2 +1 @@ -export * from './oauth.uc'; export * from './hydra-oauth.uc'; diff --git a/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts deleted file mode 100644 index 1e888abd5f1..00000000000 --- a/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts +++ /dev/null @@ -1,923 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { OauthUc } from '@modules/oauth/uc/oauth.uc'; -import { ProvisioningService } from '@modules/provisioning'; -import { ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; -import { SystemService } from '@modules/system'; -import { OauthConfigDto, SystemDto } from '@modules/system/service'; -import { UserService } from '@modules/user'; -import { UserMigrationService } from '@modules/user-login-migration'; -import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error'; -import { SchoolMigrationService } from '@modules/user-login-migration/service'; -import { MigrationDto } from '@modules/user-login-migration/service/dto'; -import { UnauthorizedException, UnprocessableEntityException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, UserDO } from '@shared/domain'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { ISession } from '@shared/domain/types/session'; -import { legacySchoolDoFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; -import { OauthCurrentUser } from '@modules/authentication/interface'; -import { AuthorizationParams } from '../controller/dto'; -import { OAuthTokenDto } from '../interface'; -import { OAuthSSOError } from '../loggable/oauth-sso.error'; -import { OAuthProcessDto } from '../service/dto'; -import { OAuthService } from '../service/oauth.service'; -import { OauthLoginStateDto } from './dto/oauth-login-state.dto'; -import resetAllMocks = jest.resetAllMocks; - -jest.mock('nanoid', () => { - return { - nanoid: () => 'mockNanoId', - }; -}); - -describe('OAuthUc', () => { - let module: TestingModule; - let uc: OauthUc; - - let authenticationService: DeepMocked; - let oauthService: DeepMocked; - let systemService: DeepMocked; - let provisioningService: DeepMocked; - let userMigrationService: DeepMocked; - let userService: DeepMocked; - let schoolMigrationService: DeepMocked; - - beforeAll(async () => { - await setupEntities(); - - module = await Test.createTestingModule({ - providers: [ - OauthUc, - { - provide: LegacyLogger, - useValue: createMock(), - }, - { - provide: SystemService, - useValue: createMock(), - }, - { - provide: OAuthService, - useValue: createMock(), - }, - { - provide: AuthenticationService, - useValue: createMock(), - }, - { - provide: ProvisioningService, - useValue: createMock(), - }, - { - provide: UserService, - useValue: createMock(), - }, - { - provide: LegacySchoolService, - useValue: createMock(), - }, - { - provide: UserMigrationService, - useValue: createMock(), - }, - { - provide: SchoolMigrationService, - useValue: createMock(), - }, - { - provide: AuthenticationService, - useValue: createMock(), - }, - ], - }).compile(); - - uc = module.get(OauthUc); - systemService = module.get(SystemService); - authenticationService = module.get(AuthenticationService); - oauthService = module.get(OAuthService); - provisioningService = module.get(ProvisioningService); - userService = module.get(UserService); - userMigrationService = module.get(UserMigrationService); - schoolMigrationService = module.get(SchoolMigrationService); - authenticationService = module.get(AuthenticationService); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - resetAllMocks(); - }); - - const createOAuthTestData = () => { - const oauthConfig: OauthConfigDto = new OauthConfigDto({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'https://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'mock_authEndpoint', - provider: 'mock_provider', - logoutEndpoint: 'mock_logoutEndpoint', - issuer: 'mock_issuer', - jwksEndpoint: 'mock_jwksEndpoint', - redirectUri: 'mock_codeRedirectUri', - }); - const system: SystemDto = new SystemDto({ - id: 'systemId', - type: 'oauth', - oauthConfig, - }); - - return { - system, - systemId: system.id as string, - oauthConfig, - }; - }; - - describe('startOauthLogin', () => { - describe('when starting an oauth login without migration', () => { - const setup = () => { - const { system, systemId } = createOAuthTestData(); - - const session: DeepMocked = createMock(); - const authenticationUrl = 'authenticationUrl'; - - systemService.findById.mockResolvedValue(system); - oauthService.getAuthenticationUrl.mockReturnValue(authenticationUrl); - - return { - systemId, - session, - authenticationUrl, - }; - }; - - it('should return the authentication url for the system', async () => { - const { systemId, session, authenticationUrl } = setup(); - - const result: string = await uc.startOauthLogin(session, systemId, false); - - expect(result).toEqual(authenticationUrl); - }); - }); - - describe('when starting an oauth login during a migration', () => { - const setup = () => { - const { system, systemId } = createOAuthTestData(); - - const session: DeepMocked = createMock(); - const authenticationUrl = 'authenticationUrl'; - const postLoginRedirect = 'postLoginRedirect'; - - systemService.findById.mockResolvedValue(system); - oauthService.getAuthenticationUrl.mockReturnValue(authenticationUrl); - - return { - system, - systemId, - postLoginRedirect, - session, - }; - }; - - it('should save data to the session', async () => { - const { systemId, system, session, postLoginRedirect } = setup(); - - await uc.startOauthLogin(session, systemId, false, postLoginRedirect); - - expect(session.oauthLoginState).toEqual({ - systemId, - state: 'mockNanoId', - postLoginRedirect, - provider: system.oauthConfig?.provider as string, - userLoginMigration: false, - }); - }); - }); - - describe('when the system cannot be found', () => { - const setup = () => { - const { systemId, system } = createOAuthTestData(); - system.oauthConfig = undefined; - const session: DeepMocked = createMock(); - const authenticationUrl = 'authenticationUrl'; - - systemService.findById.mockResolvedValue(system); - oauthService.getAuthenticationUrl.mockReturnValue(authenticationUrl); - - return { - systemId, - session, - authenticationUrl, - }; - }; - - it('should throw UnprocessableEntityException', async () => { - const { systemId, session } = setup(); - - const func = async () => uc.startOauthLogin(session, systemId, false); - - await expect(func).rejects.toThrow(UnprocessableEntityException); - }); - }); - }); - - describe('processOAuth', () => { - const setup = () => { - const postLoginRedirect = 'postLoginRedirect'; - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - postLoginRedirect, - provider: 'mock_provider', - userLoginMigration: false, - }); - const code = 'code'; - const error = 'error'; - - const jwt = 'schulcloudJwt'; - const redirect = 'redirect'; - const user: UserDO = new UserDO({ - id: 'mockUserId', - firstName: 'firstName', - lastName: 'lastame', - email: '', - roles: [], - schoolId: 'mockSchoolId', - externalId: 'mockExternalId', - }); - - const currentUser: OauthCurrentUser = { userId: 'userId', isExternalUser: true } as OauthCurrentUser; - const testSystem: SystemDto = new SystemDto({ - id: 'mockSystemId', - type: 'mock', - oauthConfig: { provider: 'testProvider' } as OauthConfigDto, - }); - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - return { cachedState, code, error, jwt, redirect, user, currentUser, testSystem, tokenDto }; - }; - - describe('when a user is returned', () => { - it('should return a response with a valid jwt', async () => { - const { cachedState, code, error, jwt, redirect, user, currentUser, tokenDto } = setup(); - - userService.getResolvedUser.mockResolvedValue(currentUser); - authenticationService.generateJwt.mockResolvedValue({ accessToken: jwt }); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - oauthService.provisionUser.mockResolvedValue({ user, redirect }); - - const response: OAuthProcessDto = await uc.processOAuthLogin(cachedState, code, error); - expect(response).toEqual( - expect.objectContaining({ - jwt, - redirect, - }) - ); - }); - }); - - describe('when no user is returned', () => { - it('should return a response without a jwt', async () => { - const { cachedState, code, error, redirect, tokenDto } = setup(); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - oauthService.provisionUser.mockResolvedValue({ redirect }); - - const response: OAuthProcessDto = await uc.processOAuthLogin(cachedState, code, error); - - expect(response).toEqual({ - redirect, - }); - }); - }); - - describe('when an error occurs', () => { - it('should return an OAuthProcessDto with error', async () => { - const { cachedState, code, error, testSystem } = setup(); - oauthService.authenticateUser.mockRejectedValue(new OAuthSSOError('Testmessage')); - systemService.findById.mockResolvedValue(testSystem); - - const response = uc.processOAuthLogin(cachedState, code, error); - - await expect(response).rejects.toThrow(OAuthSSOError); - }); - }); - - describe('when the process runs successfully', () => { - it('should return a valid jwt', async () => { - const { cachedState, code, user, currentUser, jwt, redirect, tokenDto } = setup(); - - userService.getResolvedUser.mockResolvedValue(currentUser); - authenticationService.generateJwt.mockResolvedValue({ accessToken: jwt }); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - oauthService.provisionUser.mockResolvedValue({ user, redirect }); - - const response: OAuthProcessDto = await uc.processOAuthLogin(cachedState, code); - - expect(response).toEqual({ - jwt, - redirect, - }); - }); - }); - }); - - describe('migration', () => { - describe('migrate', () => { - describe('when authorize user and migration was successful', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const oauthConfig: OauthConfigDto = new OauthConfigDto({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'https://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'mock_authEndpoint', - provider: 'mock_provider', - logoutEndpoint: 'mock_logoutEndpoint', - issuer: 'mock_issuer', - jwksEndpoint: 'mock_jwksEndpoint', - redirectUri: 'mock_codeRedirectUri', - }); - - const system: SystemDto = new SystemDto({ - id: 'systemId', - type: 'oauth', - oauthConfig, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - systemService.findById.mockResolvedValue(system); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - - return { - query, - cachedState, - }; - }; - - it('should return redirect to migration succeed page', async () => { - const { query, cachedState } = setupMigration(); - - const result: MigrationDto = await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(result.redirect).toStrictEqual('https://mock.de/migration/succeed'); - }); - - it('should remove the jwt from the whitelist', async () => { - const { query, cachedState } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(authenticationService.removeJwtFromWhitelist).toHaveBeenCalledWith('jwt'); - }); - }); - - describe('when the jwt cannot be removed', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const oauthConfig: OauthConfigDto = new OauthConfigDto({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'https://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'mock_authEndpoint', - provider: 'mock_provider', - logoutEndpoint: 'mock_logoutEndpoint', - issuer: 'mock_issuer', - jwksEndpoint: 'mock_jwksEndpoint', - redirectUri: 'mock_codeRedirectUri', - }); - - const system: SystemDto = new SystemDto({ - id: 'systemId', - type: 'oauth', - oauthConfig, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - - const error: Error = new Error('testError'); - systemService.findById.mockResolvedValue(system); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - authenticationService.removeJwtFromWhitelist.mockRejectedValue(error); - - return { - query, - cachedState, - error, - }; - }; - - it('should throw', async () => { - const { query, error, cachedState } = setupMigration(); - - const func = () => uc.migrate('jwt', 'currentUserId', query, cachedState); - - await expect(func).rejects.toThrow(error); - }); - }); - - describe('when migration failed', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - - const userMigrationFailedDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/dashboard', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - userMigrationService.migrateUser.mockResolvedValue(userMigrationFailedDto); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - - return { - query, - cachedState, - }; - }; - - it('should return redirect to dashboard ', async () => { - const { query, cachedState } = setupMigration(); - - const result: MigrationDto = await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(result.redirect).toStrictEqual('https://mock.de/dashboard'); - }); - }); - - describe('when external school and official school number is defined ', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - externalSchool: { - externalId: 'mockId', - officialSchoolNumber: 'mockNumber', - name: 'mockName', - }, - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - - return { - query, - cachedState, - oauthData, - }; - }; - - it('should call schoolToMigrate', async () => { - const { oauthData, query, cachedState } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(schoolMigrationService.schoolToMigrate).toHaveBeenCalledWith( - 'currentUserId', - oauthData.externalSchool?.externalId, - oauthData.externalSchool?.officialSchoolNumber - ); - }); - }); - - describe('when external school and official school number is defined and school has to be migrated', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - externalSchool: { - externalId: 'mockId', - officialSchoolNumber: 'mockNumber', - name: 'mockName', - }, - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - const schoolToMigrate: LegacySchoolDo | void = legacySchoolDoFactory.build({ name: 'mockName' }); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - schoolMigrationService.schoolToMigrate.mockResolvedValue(schoolToMigrate); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - return { - query, - cachedState, - oauthData, - schoolToMigrate, - }; - }; - - it('should call migrateSchool', async () => { - const { oauthData, query, cachedState, schoolToMigrate } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(schoolMigrationService.migrateSchool).toHaveBeenCalledWith( - oauthData.externalSchool?.externalId, - schoolToMigrate, - 'systemId' - ); - }); - }); - - describe('when external school and official school number is defined and school is already migrated', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - externalSchool: { - externalId: 'mockId', - officialSchoolNumber: 'mockNumber', - name: 'mockName', - }, - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - schoolMigrationService.schoolToMigrate.mockResolvedValue(null); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - return { - query, - cachedState, - }; - }; - - it('should not call migrateSchool', async () => { - const { query, cachedState } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(schoolMigrationService.migrateSchool).not.toHaveBeenCalled(); - }); - }); - - describe('when external school is not defined', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.authenticateUser.mockResolvedValue(tokenDto); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - - return { - query, - cachedState, - }; - }; - - it('should not call schoolToMigrate', async () => { - const { query, cachedState } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query, cachedState); - - expect(schoolMigrationService.schoolToMigrate).not.toHaveBeenCalled(); - }); - }); - - describe('when official school number is not defined', () => { - const setupMigration = () => { - const code = '43534543jnj543342jn2'; - - const query: AuthorizationParams = { code, state: 'state' }; - - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - externalSchool: { - externalId: 'mockId', - name: 'mockName', - }, - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - const error = new OAuthMigrationError( - 'Official school number from target migration system is missing', - 'ext_official_school_number_missing' - ); - - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - oauthService.authenticateUser.mockResolvedValue(tokenDto); - schoolMigrationService.schoolToMigrate.mockImplementation(() => { - throw error; - }); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - return { - query, - cachedState, - error, - }; - }; - - it('should throw OAuthMigrationError', async () => { - const { query, cachedState, error } = setupMigration(); - - await expect(uc.migrate('jwt', 'currentUserId', query, cachedState)).rejects.toThrow(error); - }); - }); - }); - - describe('when state is mismatched', () => { - const setupMigration = () => { - const cachedState: OauthLoginStateDto = new OauthLoginStateDto({ - state: 'state', - systemId: 'systemId', - provider: 'mock_provider', - userLoginMigration: true, - }); - - const query: AuthorizationParams = { state: 'failedState' }; - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', - }); - - oauthService.authenticateUser.mockResolvedValue(tokenDto); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - oauthService.requestToken.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - - return { - cachedState, - query, - }; - }; - - it('should throw an UnauthorizedException', async () => { - const { cachedState, query } = setupMigration(); - - const response = uc.migrate('jwt', 'currentUserId', query, cachedState); - - await expect(response).rejects.toThrow(UnauthorizedException); - }); - }); - }); -}); diff --git a/apps/server/src/modules/oauth/uc/oauth.uc.ts b/apps/server/src/modules/oauth/uc/oauth.uc.ts deleted file mode 100644 index c495e7be05d..00000000000 --- a/apps/server/src/modules/oauth/uc/oauth.uc.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Injectable, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, UserDO } from '@shared/domain'; -import { ISession } from '@shared/domain/types/session'; -import { LegacyLogger } from '@src/core/logger'; -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; -import { ProvisioningService } from '@modules/provisioning'; -import { OauthDataDto } from '@modules/provisioning/dto'; -import { SystemService } from '@modules/system'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { UserService } from '@modules/user'; -import { UserMigrationService } from '@modules/user-login-migration'; -import { SchoolMigrationService } from '@modules/user-login-migration/service'; -import { MigrationDto } from '@modules/user-login-migration/service/dto'; -import { nanoid } from 'nanoid'; -import { OauthCurrentUser } from '@modules/authentication/interface'; -import { AuthorizationParams } from '../controller/dto'; -import { OAuthTokenDto } from '../interface'; -import { OAuthProcessDto } from '../service/dto'; -import { OAuthService } from '../service/oauth.service'; -import { OauthLoginStateDto } from './dto/oauth-login-state.dto'; - -/** - * @deprecated remove after login via oauth moved to authentication module - */ -@Injectable() -export class OauthUc { - constructor( - private readonly oauthService: OAuthService, - private readonly authenticationService: AuthenticationService, - private readonly systemService: SystemService, - private readonly provisioningService: ProvisioningService, - private readonly userService: UserService, - private readonly userMigrationService: UserMigrationService, - private readonly schoolMigrationService: SchoolMigrationService, - private readonly logger: LegacyLogger - ) { - this.logger.setContext(OauthUc.name); - } - - async startOauthLogin( - session: ISession, - systemId: EntityId, - migration: boolean, - postLoginRedirect?: string - ): Promise { - const state = nanoid(16); - - const system: SystemDto = await this.systemService.findById(systemId); - if (!system.oauthConfig) { - throw new UnprocessableEntityException(`Requested system ${systemId} has no oauth configured`); - } - - const authenticationUrl: string = this.oauthService.getAuthenticationUrl(system.oauthConfig, state, migration); - - session.oauthLoginState = new OauthLoginStateDto({ - state, - systemId, - provider: system.oauthConfig.provider, - postLoginRedirect, - userLoginMigration: migration, - }); - - return authenticationUrl; - } - - async processOAuthLogin(cachedState: OauthLoginStateDto, code?: string, error?: string): Promise { - const { state, systemId, postLoginRedirect, userLoginMigration } = cachedState; - - this.logger.debug(`Oauth login process started. [state: ${state}, system: ${systemId}]`); - - const redirectUri: string = this.oauthService.getRedirectUri(userLoginMigration); - - const tokenDto: OAuthTokenDto = await this.oauthService.authenticateUser(systemId, redirectUri, code, error); - - const { user, redirect }: { user?: UserDO; redirect: string } = await this.oauthService.provisionUser( - systemId, - tokenDto.idToken, - tokenDto.accessToken, - postLoginRedirect - ); - - this.logger.debug(`Generating jwt for user. [state: ${state}, system: ${systemId}]`); - - let jwt: string | undefined; - if (user && user.id) { - jwt = await this.getJwtForUser(user.id); - } - - const response = new OAuthProcessDto({ - jwt, - redirect, - }); - - return response; - } - - async migrate( - userJwt: string, - currentUserId: string, - query: AuthorizationParams, - cachedState: OauthLoginStateDto - ): Promise { - const { state, systemId, userLoginMigration } = cachedState; - - if (state !== query.state) { - throw new UnauthorizedException(`Invalid state. Got: ${query.state} Expected: ${state}`); - } - - const redirectUri: string = this.oauthService.getRedirectUri(userLoginMigration); - - const tokenDto: OAuthTokenDto = await this.oauthService.authenticateUser( - systemId, - redirectUri, - query.code, - query.error - ); - - const data: OauthDataDto = await this.provisioningService.getData(systemId, tokenDto.idToken, tokenDto.accessToken); - - if (data.externalSchool) { - const schoolToMigrate: LegacySchoolDo | null = await this.schoolMigrationService.schoolToMigrate( - currentUserId, - data.externalSchool.externalId, - data.externalSchool.officialSchoolNumber - ); - if (schoolToMigrate) { - await this.schoolMigrationService.migrateSchool(data.externalSchool.externalId, schoolToMigrate, systemId); - } - } - - const migrationDto: MigrationDto = await this.userMigrationService.migrateUser( - currentUserId, - data.externalUser.externalId, - systemId - ); - - await this.authenticationService.removeJwtFromWhitelist(userJwt); - - return migrationDto; - } - - private async getJwtForUser(userId: EntityId): Promise { - const oauthCurrentUser: OauthCurrentUser = await this.userService.getResolvedUser(userId); - - const { accessToken } = await this.authenticationService.generateJwt(oauthCurrentUser); - - return accessToken; - } -} diff --git a/config/default.schema.json b/config/default.schema.json index bd637b719e9..ae43648f3ea 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1272,11 +1272,6 @@ "default": false, "description": "Makes the new school administration page the default page" }, - "FEATURE_CLIENT_USER_LOGIN_MIGRATION_ENABLED": { - "type": "boolean", - "default": false, - "description": "Changes the schulcloud client to use new login endpoints" - }, "FEATURE_CTL_TOOLS_TAB_ENABLED": { "type": "boolean", "default": false, diff --git a/src/services/config/publicAppConfigService.js b/src/services/config/publicAppConfigService.js index 06a54c6cf96..62615f0efb1 100644 --- a/src/services/config/publicAppConfigService.js +++ b/src/services/config/publicAppConfigService.js @@ -56,7 +56,6 @@ const exposedVars = [ 'FEATURE_ALLOW_INSECURE_LDAP_URL_ENABLED', 'FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED', 'MIGRATION_END_GRACE_PERIOD_MS', - 'FEATURE_CLIENT_USER_LOGIN_MIGRATION_ENABLED', 'FEATURE_CTL_TOOLS_TAB_ENABLED', 'FEATURE_LTI_TOOLS_TAB_ENABLED', 'FILES_STORAGE__MAX_FILE_SIZE', From e806f1c6e4fd6090471069bef73a10a88b583f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Tue, 14 Nov 2023 08:59:38 +0100 Subject: [PATCH 18/23] N21-1311 Refactor User login migration 1 (#4548) --- .../src/core/logger/types/logging.types.ts | 2 +- .../modules/authentication/errors/index.ts | 1 - .../modules/authentication/loggable/index.ts | 1 + ...ol-in-migration.loggable-exception.spec.ts | 24 + ...school-in-migration.loggable-exception.ts} | 15 +- .../strategy/oauth2.strategy.spec.ts | 18 +- .../strategy/oauth2.strategy.ts | 16 +- .../legacy-school/controller/dto/index.ts | 3 - .../controller/dto/migration.body.ts | 31 - .../controller/dto/migration.response.ts | 40 -- .../controller/dto/school.params.ts | 12 - .../legacy-school.controller.spec.ts | 94 --- .../controller/legacy-school.controller.ts | 61 -- .../legacy-school/legacy-school-api.module.ts | 18 - .../{error => loggable}/index.ts | 0 ...umber-duplicate.loggable-exception.spec.ts | 0 ...ool-number-duplicate.loggable-exception.ts | 0 .../mapper/migration.mapper.spec.ts | 51 -- .../legacy-school/mapper/migration.mapper.ts | 18 - .../school-validation.service.spec.ts | 4 +- .../validation/school-validation.service.ts | 2 +- .../uc/dto/oauth-migration.dto.ts | 19 - .../src/modules/legacy-school/uc/index.ts | 1 - .../legacy-school/uc/legacy-school.uc.spec.ts | 285 -------- .../legacy-school/uc/legacy-school.uc.ts | 105 --- .../src/modules/oauth/controller/dto/index.ts | 3 - .../oauth/controller/dto/sso-login.query.ts | 14 - .../oauth/controller/dto/system-id.params.ts | 12 - .../controller/dto/user-migration.response.ts | 7 - .../controller/oauth-sso.controller.spec.ts | 6 +- .../oauth/mapper/user-migration.mapper.ts | 12 - .../src/modules/oauth/oauth-api.module.ts | 19 +- apps/server/src/modules/oauth/oauth.module.ts | 12 +- .../oauth/service/oauth.service.spec.ts | 629 ++++++++++++------ .../modules/oauth/service/oauth.service.ts | 68 +- .../src/modules/server/server.module.ts | 10 +- .../api-test/user-login-migration.api.spec.ts | 21 +- .../controller/dto/index.ts | 2 - .../dto/request/page-type.query.param.ts | 17 - .../dto/response/page-content.response.ts | 18 - .../user-login-migration.controller.ts | 2 +- .../user-migration.controller.spec.ts | 67 -- .../controller/user-migration.controller.ts | 27 - .../user-login-migration/error/index.ts | 7 - .../error/oauth-migration.error.spec.ts | 31 - .../error/oauth-migration.error.ts | 28 - .../error/school-migration.error.ts | 17 - .../error/user-login-migration.error.ts | 16 - .../src/modules/user-login-migration/index.ts | 1 - .../interface/page-types.enum.ts | 5 - .../loggable/debug/index.ts | 3 + ...hool-migration-successful.loggable.spec.ts | 42 ++ .../school-migration-successful.loggable.ts | 18 + .../user-migration-started.loggable.spec.ts | 34 + .../debug/user-migration-started.loggable.ts | 16 + ...user-migration-successful.loggable.spec.ts | 34 + .../user-migration-successful.loggable.ts | 16 + ...-number-missing.loggable-exception.spec.ts | 30 + ...chool-number-missing.loggable-exception.ts | 19 + .../user-login-migration/loggable/index.ts | 10 + ...login-migration.loggable-exception.spec.ts | 35 + ...user-login-migration.loggable-exception.ts | 21 + ...peration-failed.loggable-exception.spec.ts | 32 + ...ase-operation-failed.loggable-exception.ts | 29 + ...number-mismatch.loggable-exception.spec.ts | 34 + ...hool-number-mismatch.loggable-exception.ts | 32 + ...-number-missing.loggable-exception.spec.ts | 32 + ...chool-number-missing.loggable-exception.ts | 0 ...ation-already-closed.loggable-exception.ts | 0 ...grace-period-expired-loggable.exception.ts | 0 ...-migration-not-found.loggable-exception.ts | 0 ...peration-failed.loggable-exception.spec.ts | 32 + ...ase-operation-failed.loggable-exception.ts | 24 + .../user-login-migration/mapper/index.ts | 1 - .../mapper/page-content.mapper.spec.ts | 38 -- .../mapper/page-content.mapper.ts | 15 - .../mapper/user-login-migration.mapper.ts | 2 + .../service/school-migration.service.spec.ts | 424 +++++------- .../service/school-migration.service.ts | 168 ++--- .../user-login-migration.service.spec.ts | 605 ++++------------- .../service/user-login-migration.service.ts | 140 ++-- .../service/user-migration.service.spec.ts | 492 ++++---------- .../service/user-migration.service.ts | 174 +---- .../uc/close-user-login-migration.uc.spec.ts | 93 +-- .../uc/close-user-login-migration.uc.ts | 48 +- .../restart-user-login-migration.uc.spec.ts | 168 ++--- .../uc/restart-user-login-migration.uc.ts | 51 +- .../uc/start-user-login-migration.uc.spec.ts | 10 +- .../uc/start-user-login-migration.uc.ts | 13 +- .../uc/toggle-user-login-migration.uc.spec.ts | 10 +- .../uc/toggle-user-login-migration.uc.ts | 10 +- .../uc/user-login-migration.uc.spec.ts | 568 +++++----------- .../uc/user-login-migration.uc.ts | 115 +--- .../user-login-migration-api.module.ts | 11 +- 94 files changed, 1936 insertions(+), 3585 deletions(-) create mode 100644 apps/server/src/modules/authentication/loggable/index.ts create mode 100644 apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.spec.ts rename apps/server/src/modules/authentication/{errors/school-in-migration.error.ts => loggable/school-in-migration.loggable-exception.ts} (51%) delete mode 100644 apps/server/src/modules/legacy-school/controller/dto/index.ts delete mode 100644 apps/server/src/modules/legacy-school/controller/dto/migration.body.ts delete mode 100644 apps/server/src/modules/legacy-school/controller/dto/migration.response.ts delete mode 100644 apps/server/src/modules/legacy-school/controller/dto/school.params.ts delete mode 100644 apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts delete mode 100644 apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts delete mode 100644 apps/server/src/modules/legacy-school/legacy-school-api.module.ts rename apps/server/src/modules/legacy-school/{error => loggable}/index.ts (100%) rename apps/server/src/modules/legacy-school/{error => loggable}/school-number-duplicate.loggable-exception.spec.ts (100%) rename apps/server/src/modules/legacy-school/{error => loggable}/school-number-duplicate.loggable-exception.ts (100%) delete mode 100644 apps/server/src/modules/legacy-school/mapper/migration.mapper.spec.ts delete mode 100644 apps/server/src/modules/legacy-school/mapper/migration.mapper.ts delete mode 100644 apps/server/src/modules/legacy-school/uc/dto/oauth-migration.dto.ts delete mode 100644 apps/server/src/modules/legacy-school/uc/index.ts delete mode 100644 apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts delete mode 100644 apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts delete mode 100644 apps/server/src/modules/oauth/controller/dto/sso-login.query.ts delete mode 100644 apps/server/src/modules/oauth/controller/dto/system-id.params.ts delete mode 100644 apps/server/src/modules/oauth/controller/dto/user-migration.response.ts delete mode 100644 apps/server/src/modules/oauth/mapper/user-migration.mapper.ts delete mode 100644 apps/server/src/modules/user-login-migration/controller/dto/request/page-type.query.param.ts delete mode 100644 apps/server/src/modules/user-login-migration/controller/dto/response/page-content.response.ts delete mode 100644 apps/server/src/modules/user-login-migration/controller/user-migration.controller.spec.ts delete mode 100644 apps/server/src/modules/user-login-migration/controller/user-migration.controller.ts delete mode 100644 apps/server/src/modules/user-login-migration/error/index.ts delete mode 100644 apps/server/src/modules/user-login-migration/error/oauth-migration.error.spec.ts delete mode 100644 apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts delete mode 100644 apps/server/src/modules/user-login-migration/error/school-migration.error.ts delete mode 100644 apps/server/src/modules/user-login-migration/error/user-login-migration.error.ts delete mode 100644 apps/server/src/modules/user-login-migration/interface/page-types.enum.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/index.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/school-number-missing.loggable-exception.spec.ts rename apps/server/src/modules/user-login-migration/{error => loggable}/school-number-missing.loggable-exception.ts (100%) rename apps/server/src/modules/user-login-migration/{error => loggable}/user-login-migration-already-closed.loggable-exception.ts (100%) rename apps/server/src/modules/user-login-migration/{error => loggable}/user-login-migration-grace-period-expired-loggable.exception.ts (100%) rename apps/server/src/modules/user-login-migration/{error => loggable}/user-login-migration-not-found.loggable-exception.ts (100%) create mode 100644 apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.ts delete mode 100644 apps/server/src/modules/user-login-migration/mapper/page-content.mapper.spec.ts delete mode 100644 apps/server/src/modules/user-login-migration/mapper/page-content.mapper.ts diff --git a/apps/server/src/core/logger/types/logging.types.ts b/apps/server/src/core/logger/types/logging.types.ts index e8c27380b6f..5271ba85338 100644 --- a/apps/server/src/core/logger/types/logging.types.ts +++ b/apps/server/src/core/logger/types/logging.types.ts @@ -7,7 +7,7 @@ export type ErrorLogMessage = { error?: Error; type: string; // TODO: use enum stack?: string; - data?: { [key: string]: string | number | undefined }; + data?: { [key: string]: string | number | boolean | undefined }; }; export type ValidationErrorLogMessage = { diff --git a/apps/server/src/modules/authentication/errors/index.ts b/apps/server/src/modules/authentication/errors/index.ts index d87d53df8bf..d345cf9b0ce 100644 --- a/apps/server/src/modules/authentication/errors/index.ts +++ b/apps/server/src/modules/authentication/errors/index.ts @@ -1,4 +1,3 @@ export * from './brute-force.error'; export * from './ldap-connection.error'; -export * from './school-in-migration.error'; export * from './unauthorized.loggable-exception'; diff --git a/apps/server/src/modules/authentication/loggable/index.ts b/apps/server/src/modules/authentication/loggable/index.ts new file mode 100644 index 00000000000..7e6fcda9db1 --- /dev/null +++ b/apps/server/src/modules/authentication/loggable/index.ts @@ -0,0 +1 @@ +export * from './school-in-migration.loggable-exception'; diff --git a/apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.spec.ts b/apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.spec.ts new file mode 100644 index 00000000000..49f330efa17 --- /dev/null +++ b/apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.spec.ts @@ -0,0 +1,24 @@ +import { SchoolInMigrationLoggableException } from './school-in-migration.loggable-exception'; + +describe(SchoolInMigrationLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const exception = new SchoolInMigrationLoggableException(); + + return { + exception, + }; + }; + + it('should return the correct log message', () => { + const { exception } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'SCHOOL_IN_MIGRATION', + stack: expect.any(String), + }); + }); + }); +}); diff --git a/apps/server/src/modules/authentication/errors/school-in-migration.error.ts b/apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.ts similarity index 51% rename from apps/server/src/modules/authentication/errors/school-in-migration.error.ts rename to apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.ts index ed507b07656..76f5baecb61 100644 --- a/apps/server/src/modules/authentication/errors/school-in-migration.error.ts +++ b/apps/server/src/modules/authentication/loggable/school-in-migration.loggable-exception.ts @@ -1,16 +1,23 @@ import { HttpStatus } from '@nestjs/common'; import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; -export class SchoolInMigrationError extends BusinessError { - constructor(details?: Record) { +export class SchoolInMigrationLoggableException extends BusinessError implements Loggable { + constructor() { super( { type: 'SCHOOL_IN_MIGRATION', title: 'Login failed because school is in migration', defaultMessage: 'Login failed because creation of user is not possible during migration', }, - HttpStatus.UNAUTHORIZED, - details + HttpStatus.UNAUTHORIZED ); } + + getLogMessage(): ErrorLogMessage { + return { + type: this.type, + stack: this.stack, + }; + } } diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts index f67f620175d..e6bf4ef2fa6 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts @@ -1,15 +1,15 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { OAuthTokenDto } from '@modules/oauth'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, RoleName } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { userDoFactory } from '@shared/testing'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; -import { SchoolInMigrationError } from '../errors/school-in-migration.error'; import { ICurrentUser, OauthCurrentUser } from '../interface'; +import { SchoolInMigrationLoggableException } from '../loggable'; import { Oauth2Strategy } from './oauth2.strategy'; describe('Oauth2Strategy', () => { @@ -68,7 +68,7 @@ describe('Oauth2Strategy', () => { refreshToken: 'refreshToken', }) ); - oauthService.provisionUser.mockResolvedValue({ user, redirect: '' }); + oauthService.provisionUser.mockResolvedValue(user); accountService.findByUserId.mockResolvedValue(account); return { systemId, user, account, idToken }; @@ -102,7 +102,7 @@ describe('Oauth2Strategy', () => { refreshToken: 'refreshToken', }) ); - oauthService.provisionUser.mockResolvedValue({ user: undefined, redirect: '' }); + oauthService.provisionUser.mockResolvedValue(null); }; it('should throw a SchoolInMigrationError', async () => { @@ -111,7 +111,7 @@ describe('Oauth2Strategy', () => { const func = async () => strategy.validate({ body: { code: 'code', redirectUri: 'redirectUri', systemId: 'systemId' } }); - await expect(func).rejects.toThrow(new SchoolInMigrationError()); + await expect(func).rejects.toThrow(new SchoolInMigrationLoggableException()); }); }); @@ -126,7 +126,7 @@ describe('Oauth2Strategy', () => { refreshToken: 'refreshToken', }) ); - oauthService.provisionUser.mockResolvedValue({ user, redirect: '' }); + oauthService.provisionUser.mockResolvedValue(user); accountService.findByUserId.mockResolvedValue(null); }; diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts index 599744cc1a7..e83e9174abc 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts @@ -1,14 +1,14 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { UserDO } from '@shared/domain/domainobject/user.do'; import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; import { OAuthTokenDto } from '@modules/oauth'; import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { Strategy } from 'passport-custom'; import { Oauth2AuthorizationBodyParams } from '../controllers/dto'; -import { SchoolInMigrationError } from '../errors/school-in-migration.error'; import { ICurrentUser, OauthCurrentUser } from '../interface'; +import { SchoolInMigrationLoggableException } from '../loggable'; import { CurrentUserMapper } from '../mapper'; @Injectable() @@ -22,14 +22,10 @@ export class Oauth2Strategy extends PassportStrategy(Strategy, 'oauth2') { const tokenDto: OAuthTokenDto = await this.oauthService.authenticateUser(systemId, redirectUri, code); - const { user }: { user?: UserDO; redirect: string } = await this.oauthService.provisionUser( - systemId, - tokenDto.idToken, - tokenDto.accessToken - ); + const user: UserDO | null = await this.oauthService.provisionUser(systemId, tokenDto.idToken, tokenDto.accessToken); if (!user || !user.id) { - throw new SchoolInMigrationError(); + throw new SchoolInMigrationLoggableException(); } const account: AccountDto | null = await this.accountService.findByUserId(user.id); diff --git a/apps/server/src/modules/legacy-school/controller/dto/index.ts b/apps/server/src/modules/legacy-school/controller/dto/index.ts deleted file mode 100644 index a6e114b1f29..00000000000 --- a/apps/server/src/modules/legacy-school/controller/dto/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './migration.body'; -export * from './migration.response'; -export * from './school.params'; diff --git a/apps/server/src/modules/legacy-school/controller/dto/migration.body.ts b/apps/server/src/modules/legacy-school/controller/dto/migration.body.ts deleted file mode 100644 index b598b78942c..00000000000 --- a/apps/server/src/modules/legacy-school/controller/dto/migration.body.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsOptional } from 'class-validator'; - -export class MigrationBody { - @IsBoolean() - @IsOptional() - @ApiProperty({ - description: 'Set if migration is possible in this school', - required: false, - nullable: true, - }) - oauthMigrationPossible?: boolean; - - @IsBoolean() - @IsOptional() - @ApiProperty({ - description: 'Set if migration is mandatory in this school', - required: false, - nullable: true, - }) - oauthMigrationMandatory?: boolean; - - @IsBoolean() - @IsOptional() - @ApiProperty({ - description: 'Set if migration is finished in this school', - required: false, - nullable: true, - }) - oauthMigrationFinished?: boolean; -} diff --git a/apps/server/src/modules/legacy-school/controller/dto/migration.response.ts b/apps/server/src/modules/legacy-school/controller/dto/migration.response.ts deleted file mode 100644 index 7a7aea8445c..00000000000 --- a/apps/server/src/modules/legacy-school/controller/dto/migration.response.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class MigrationResponse { - @ApiPropertyOptional({ - description: 'Date from when Migration is possible', - type: Date, - }) - oauthMigrationPossible?: Date; - - @ApiPropertyOptional({ - description: 'Date from when Migration is mandatory', - type: Date, - }) - oauthMigrationMandatory?: Date; - - @ApiPropertyOptional({ - description: 'Date from when Migration is finished', - type: Date, - }) - oauthMigrationFinished?: Date; - - @ApiPropertyOptional({ - description: 'Date from when Migration is finally finished and cannot be restarted again', - type: Date, - }) - oauthMigrationFinalFinish?: Date; - - @ApiProperty({ - description: 'Enable the Migration', - }) - enableMigrationStart!: boolean; - - constructor(params: MigrationResponse) { - this.oauthMigrationPossible = params.oauthMigrationPossible; - this.oauthMigrationMandatory = params.oauthMigrationMandatory; - this.oauthMigrationFinished = params.oauthMigrationFinished; - this.oauthMigrationFinalFinish = params.oauthMigrationFinalFinish; - this.enableMigrationStart = params.enableMigrationStart; - } -} diff --git a/apps/server/src/modules/legacy-school/controller/dto/school.params.ts b/apps/server/src/modules/legacy-school/controller/dto/school.params.ts deleted file mode 100644 index 248ac3861b5..00000000000 --- a/apps/server/src/modules/legacy-school/controller/dto/school.params.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsMongoId } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class SchoolParams { - @IsMongoId() - @ApiProperty({ - description: 'The id of the school.', - required: true, - nullable: false, - }) - schoolId!: string; -} diff --git a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts b/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts deleted file mode 100644 index 764d71b6abf..00000000000 --- a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { MigrationMapper } from '../mapper/migration.mapper'; -import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; -import { LegacySchoolUc } from '../uc'; -import { MigrationBody, MigrationResponse, SchoolParams } from './dto'; -import { LegacySchoolController } from './legacy-school.controller'; - -describe('Legacy School Controller', () => { - let module: TestingModule; - let controller: LegacySchoolController; - let schoolUc: DeepMocked; - let mapper: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - controllers: [LegacySchoolController], - providers: [ - { - provide: LegacySchoolUc, - useValue: createMock(), - }, - { - provide: MigrationMapper, - useValue: createMock(), - }, - ], - }).compile(); - controller = module.get(LegacySchoolController); - schoolUc = module.get(LegacySchoolUc); - mapper = module.get(MigrationMapper); - }); - - afterAll(async () => { - await module.close(); - jest.clearAllMocks(); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - const setupBasicData = () => { - const schoolParams: SchoolParams = { schoolId: new ObjectId().toHexString() }; - const testUser: ICurrentUser = { userId: 'testUser' } as ICurrentUser; - - const migrationResp: MigrationResponse = { - oauthMigrationMandatory: new Date(), - oauthMigrationPossible: new Date(), - oauthMigrationFinished: new Date(), - enableMigrationStart: true, - }; - const migrationDto: OauthMigrationDto = new OauthMigrationDto({ - oauthMigrationMandatory: new Date(), - oauthMigrationPossible: new Date(), - oauthMigrationFinished: new Date(), - enableMigrationStart: true, - }); - return { schoolParams, testUser, migrationDto, migrationResp }; - }; - - describe('setMigration', () => { - describe('when migrationflags exist and schoolId and userId are given', () => { - it('should call UC and recieve a response', async () => { - const { schoolParams, testUser, migrationDto, migrationResp } = setupBasicData(); - schoolUc.setMigration.mockResolvedValue(migrationDto); - mapper.mapDtoToResponse.mockReturnValue(migrationResp); - const body: MigrationBody = { oauthMigrationPossible: true, oauthMigrationMandatory: true }; - - const res: MigrationResponse = await controller.setMigration(schoolParams, body, testUser); - - expect(schoolUc.setMigration).toHaveBeenCalled(); - expect(res).toBe(migrationResp); - }); - }); - }); - - describe('getMigration', () => { - describe('when schoolId and UserId are given', () => { - it('should call UC and recieve a response', async () => { - const { schoolParams, testUser, migrationDto, migrationResp } = setupBasicData(); - schoolUc.getMigration.mockResolvedValue(migrationDto); - mapper.mapDtoToResponse.mockReturnValue(migrationResp); - - const res: MigrationResponse = await controller.getMigration(schoolParams, testUser); - - expect(schoolUc.getMigration).toHaveBeenCalled(); - expect(res).toBe(migrationResp); - }); - }); - }); -}); diff --git a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts b/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts deleted file mode 100644 index 58b591faea1..00000000000 --- a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Body, Controller, Get, Param, Put } from '@nestjs/common'; -import { - ApiFoundResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; -import { MigrationMapper } from '../mapper/migration.mapper'; -import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; -import { LegacySchoolUc } from '../uc'; -import { MigrationBody, MigrationResponse, SchoolParams } from './dto'; - -/** - * @deprecated because it uses the deprecated LegacySchoolDo. - */ -@ApiTags('School') -@Authenticate('jwt') -@Controller('school') -export class LegacySchoolController { - constructor(private readonly schoolUc: LegacySchoolUc, private readonly migrationMapper: MigrationMapper) {} - - @Put(':schoolId/migration') - @Authenticate('jwt') - @ApiOkResponse({ description: 'New migrationflags set', type: MigrationResponse }) - @ApiUnauthorizedResponse() - async setMigration( - @Param() schoolParams: SchoolParams, - @Body() migrationBody: MigrationBody, - @CurrentUser() currentUser: ICurrentUser - ): Promise { - const migrationDto: OauthMigrationDto = await this.schoolUc.setMigration( - schoolParams.schoolId, - !!migrationBody.oauthMigrationPossible, - !!migrationBody.oauthMigrationMandatory, - !!migrationBody.oauthMigrationFinished, - currentUser.userId - ); - - const result: MigrationResponse = this.migrationMapper.mapDtoToResponse(migrationDto); - - return result; - } - - @Get(':schoolId/migration') - @Authenticate('jwt') - @ApiFoundResponse({ description: 'Migrationflags have been found.', type: MigrationResponse }) - @ApiUnauthorizedResponse() - @ApiNotFoundResponse({ description: 'Migrationsflags could not be found for the given school' }) - async getMigration( - @Param() schoolParams: SchoolParams, - @CurrentUser() currentUser: ICurrentUser - ): Promise { - const migrationDto: OauthMigrationDto = await this.schoolUc.getMigration(schoolParams.schoolId, currentUser.userId); - - const result: MigrationResponse = this.migrationMapper.mapDtoToResponse(migrationDto); - - return result; - } -} diff --git a/apps/server/src/modules/legacy-school/legacy-school-api.module.ts b/apps/server/src/modules/legacy-school/legacy-school-api.module.ts deleted file mode 100644 index aaf1f6acad2..00000000000 --- a/apps/server/src/modules/legacy-school/legacy-school-api.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AuthorizationModule } from '@modules/authorization'; -import { LoggerModule } from '@src/core/logger'; -import { UserLoginMigrationModule } from '@modules/user-login-migration'; -import { LegacySchoolUc } from './uc'; -import { LegacySchoolModule } from './legacy-school.module'; -import { LegacySchoolController } from './controller/legacy-school.controller'; -import { MigrationMapper } from './mapper/migration.mapper'; - -/** - * @deprecated because it uses the deprecated LegacySchoolDo. - */ -@Module({ - imports: [LegacySchoolModule, AuthorizationModule, LoggerModule, UserLoginMigrationModule], - controllers: [LegacySchoolController], - providers: [LegacySchoolUc, MigrationMapper], -}) -export class LegacySchoolApiModule {} diff --git a/apps/server/src/modules/legacy-school/error/index.ts b/apps/server/src/modules/legacy-school/loggable/index.ts similarity index 100% rename from apps/server/src/modules/legacy-school/error/index.ts rename to apps/server/src/modules/legacy-school/loggable/index.ts diff --git a/apps/server/src/modules/legacy-school/error/school-number-duplicate.loggable-exception.spec.ts b/apps/server/src/modules/legacy-school/loggable/school-number-duplicate.loggable-exception.spec.ts similarity index 100% rename from apps/server/src/modules/legacy-school/error/school-number-duplicate.loggable-exception.spec.ts rename to apps/server/src/modules/legacy-school/loggable/school-number-duplicate.loggable-exception.spec.ts diff --git a/apps/server/src/modules/legacy-school/error/school-number-duplicate.loggable-exception.ts b/apps/server/src/modules/legacy-school/loggable/school-number-duplicate.loggable-exception.ts similarity index 100% rename from apps/server/src/modules/legacy-school/error/school-number-duplicate.loggable-exception.ts rename to apps/server/src/modules/legacy-school/loggable/school-number-duplicate.loggable-exception.ts diff --git a/apps/server/src/modules/legacy-school/mapper/migration.mapper.spec.ts b/apps/server/src/modules/legacy-school/mapper/migration.mapper.spec.ts deleted file mode 100644 index cc683d2acaa..00000000000 --- a/apps/server/src/modules/legacy-school/mapper/migration.mapper.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MigrationResponse } from '../controller/dto'; -import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; -import { MigrationMapper } from './migration.mapper'; - -describe('MigrationMapper', () => { - let mapper: MigrationMapper; - let module: TestingModule; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [MigrationMapper], - }).compile(); - mapper = module.get(MigrationMapper); - }); - - afterAll(async () => { - await module.close(); - }); - describe('when it maps migration data', () => { - const setup = () => { - const dto: OauthMigrationDto = new OauthMigrationDto({ - oauthMigrationPossible: new Date('2023-01-23T09:34:54.854Z'), - oauthMigrationMandatory: new Date('2023-02-23T09:34:54.854Z'), - oauthMigrationFinished: new Date('2023-03-23T09:34:54.854Z'), - oauthMigrationFinalFinish: new Date('2023-04-23T09:34:54.854Z'), - enableMigrationStart: true, - }); - const response: MigrationResponse = new MigrationResponse({ - oauthMigrationPossible: new Date('2023-01-23T09:34:54.854Z'), - oauthMigrationMandatory: new Date('2023-02-23T09:34:54.854Z'), - oauthMigrationFinished: new Date('2023-03-23T09:34:54.854Z'), - oauthMigrationFinalFinish: new Date('2023-04-23T09:34:54.854Z'), - enableMigrationStart: true, - }); - - return { - dto, - response, - }; - }; - - it('mapToDO', () => { - const { dto, response } = setup(); - - const result: MigrationResponse = mapper.mapDtoToResponse(dto); - - expect(result).toEqual(response); - }); - }); -}); diff --git a/apps/server/src/modules/legacy-school/mapper/migration.mapper.ts b/apps/server/src/modules/legacy-school/mapper/migration.mapper.ts deleted file mode 100644 index 3fd152fbc2b..00000000000 --- a/apps/server/src/modules/legacy-school/mapper/migration.mapper.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { MigrationResponse } from '../controller/dto'; -import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; - -@Injectable() -export class MigrationMapper { - public mapDtoToResponse(dto: OauthMigrationDto): MigrationResponse { - const response: MigrationResponse = new MigrationResponse({ - oauthMigrationPossible: dto.oauthMigrationPossible, - oauthMigrationMandatory: dto.oauthMigrationMandatory, - oauthMigrationFinished: dto.oauthMigrationFinished, - oauthMigrationFinalFinish: dto.oauthMigrationFinalFinish, - enableMigrationStart: dto.enableMigrationStart, - }); - - return response; - } -} diff --git a/apps/server/src/modules/legacy-school/service/validation/school-validation.service.spec.ts b/apps/server/src/modules/legacy-school/service/validation/school-validation.service.spec.ts index aa01608fb9f..065a65fe446 100644 --- a/apps/server/src/modules/legacy-school/service/validation/school-validation.service.spec.ts +++ b/apps/server/src/modules/legacy-school/service/validation/school-validation.service.spec.ts @@ -3,10 +3,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain'; import { LegacySchoolRepo } from '@shared/repo'; import { legacySchoolDoFactory } from '@shared/testing'; -import { SchoolNumberDuplicateLoggableException } from '../../error'; +import { SchoolNumberDuplicateLoggableException } from '../../loggable'; import { SchoolValidationService } from './school-validation.service'; -describe('SchoolValidationService', () => { +describe(SchoolValidationService.name, () => { let module: TestingModule; let service: SchoolValidationService; diff --git a/apps/server/src/modules/legacy-school/service/validation/school-validation.service.ts b/apps/server/src/modules/legacy-school/service/validation/school-validation.service.ts index 044e0473ef3..e934b770407 100644 --- a/apps/server/src/modules/legacy-school/service/validation/school-validation.service.ts +++ b/apps/server/src/modules/legacy-school/service/validation/school-validation.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { LegacySchoolDo } from '@shared/domain'; import { LegacySchoolRepo } from '@shared/repo'; -import { SchoolNumberDuplicateLoggableException } from '../../error'; +import { SchoolNumberDuplicateLoggableException } from '../../loggable'; @Injectable() export class SchoolValidationService { diff --git a/apps/server/src/modules/legacy-school/uc/dto/oauth-migration.dto.ts b/apps/server/src/modules/legacy-school/uc/dto/oauth-migration.dto.ts deleted file mode 100644 index c88bcab9ac9..00000000000 --- a/apps/server/src/modules/legacy-school/uc/dto/oauth-migration.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -export class OauthMigrationDto { - oauthMigrationPossible?: Date; - - oauthMigrationMandatory?: Date; - - oauthMigrationFinished?: Date; - - oauthMigrationFinalFinish?: Date; - - enableMigrationStart!: boolean; - - constructor(params: OauthMigrationDto) { - this.oauthMigrationPossible = params.oauthMigrationPossible; - this.oauthMigrationMandatory = params.oauthMigrationMandatory; - this.oauthMigrationFinished = params.oauthMigrationFinished; - this.oauthMigrationFinalFinish = params.oauthMigrationFinalFinish; - this.enableMigrationStart = params.enableMigrationStart; - } -} diff --git a/apps/server/src/modules/legacy-school/uc/index.ts b/apps/server/src/modules/legacy-school/uc/index.ts deleted file mode 100644 index 97a341c0458..00000000000 --- a/apps/server/src/modules/legacy-school/uc/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './legacy-school.uc'; diff --git a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts b/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts deleted file mode 100644 index 138bcd81a0a..00000000000 --- a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { UnprocessableEntityException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain'; -import { legacySchoolDoFactory, userLoginMigrationDOFactory } from '@shared/testing/factory'; -import { AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school/service'; -import { LegacySchoolUc } from '@modules/legacy-school/uc'; -import { - SchoolMigrationService, - UserLoginMigrationRevertService, - UserLoginMigrationService, -} from '@modules/user-login-migration'; -import { OauthMigrationDto } from './dto/oauth-migration.dto'; - -describe('LegacySchoolUc', () => { - let module: TestingModule; - let schoolUc: LegacySchoolUc; - - let schoolService: DeepMocked; - let authService: DeepMocked; - let schoolMigrationService: DeepMocked; - let userLoginMigrationService: DeepMocked; - let userLoginMigrationRevertService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - LegacySchoolUc, - { - provide: LegacySchoolService, - useValue: createMock(), - }, - { - provide: AuthorizationService, - useValue: createMock(), - }, - { - provide: SchoolMigrationService, - useValue: createMock(), - }, - { - provide: UserLoginMigrationService, - useValue: createMock(), - }, - { - provide: UserLoginMigrationRevertService, - useValue: createMock(), - }, - ], - }).compile(); - - schoolService = module.get(LegacySchoolService); - authService = module.get(AuthorizationService); - schoolUc = module.get(LegacySchoolUc); - schoolMigrationService = module.get(SchoolMigrationService); - userLoginMigrationService = module.get(UserLoginMigrationService); - userLoginMigrationRevertService = module.get(UserLoginMigrationRevertService); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - // Tests with case of authService.checkPermission.mockImplementation(() => throw new ForbiddenException()); - // are missed for both methodes - - describe('setMigration is called', () => { - describe('when first starting the migration', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', - startedAt: new Date('2023-05-02'), - }); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); - authService.checkPermission.mockReturnValueOnce(); - schoolService.getSchoolById.mockResolvedValue(school); - userLoginMigrationService.setMigration.mockResolvedValue(userLoginMigration); - }; - - it('should return the migration dto', async () => { - setup(); - - const result: OauthMigrationDto = await schoolUc.setMigration('schoolId', true, false, false, 'userId'); - - expect(result).toEqual({ - oauthMigrationPossible: new Date('2023-05-02'), - enableMigrationStart: true, - }); - }); - }); - - describe('when closing a migration', () => { - describe('when there were migrated users', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.build({ - closedAt: undefined, - }); - const updatedUserLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.build({ - targetSystemId: userLoginMigration.targetSystemId, - closedAt: new Date(2023, 5), - }); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermission.mockReturnValueOnce(); - schoolService.getSchoolById.mockResolvedValue(school); - userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); - schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(true); - - return { - updatedUserLoginMigration, - }; - }; - - it('should call schoolMigrationService.markUnmigratedUsersAsOutdated', async () => { - const { updatedUserLoginMigration } = setup(); - - await schoolUc.setMigration(updatedUserLoginMigration.schoolId, true, false, true, 'userId'); - - expect(schoolMigrationService.markUnmigratedUsersAsOutdated).toHaveBeenCalled(); - }); - }); - - describe('when there were no users migrated', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.build({ - closedAt: undefined, - }); - const updatedUserLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.build({ - targetSystemId: userLoginMigration.targetSystemId, - closedAt: new Date(2023, 5), - }); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermission.mockReturnValueOnce(); - schoolService.getSchoolById.mockResolvedValue(school); - userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); - schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(false); - - return { - updatedUserLoginMigration, - }; - }; - - it('should call userLoginMigrationRevertService.revertUserLoginMigration', async () => { - const { updatedUserLoginMigration } = setup(); - - await schoolUc.setMigration(updatedUserLoginMigration.schoolId, true, false, true, 'userId'); - - expect(userLoginMigrationRevertService.revertUserLoginMigration).toHaveBeenCalledWith( - updatedUserLoginMigration - ); - }); - }); - }); - - describe('when restarting a migration', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', - startedAt: new Date('2023-05-02'), - closedAt: new Date('2023-05-02'), - finishedAt: new Date('2023-05-02'), - }); - const updatedUserLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', - startedAt: new Date('2023-05-02'), - }); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermission.mockReturnValueOnce(); - schoolService.getSchoolById.mockResolvedValue(school); - userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); - }; - - it('should call schoolMigrationService.unmarkOutdatedUsers', async () => { - setup(); - - await schoolUc.setMigration('schoolId', true, false, false, 'userId'); - - expect(schoolMigrationService.unmarkOutdatedUsers).toHaveBeenCalled(); - }); - }); - - describe('when trying to start a finished migration after the grace period', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', - startedAt: new Date('2023-05-02'), - closedAt: new Date('2023-05-02'), - finishedAt: new Date('2023-05-02'), - }); - const updatedUserLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', - startedAt: new Date('2023-05-02'), - }); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermission.mockReturnValueOnce(); - schoolService.getSchoolById.mockResolvedValue(school); - userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); - schoolMigrationService.validateGracePeriod.mockImplementation(() => { - throw new UnprocessableEntityException(); - }); - }; - - it('should throw an error', async () => { - setup(); - - const func = async () => schoolUc.setMigration('schoolId', true, false, false, 'userId'); - - await expect(func).rejects.toThrow(UnprocessableEntityException); - }); - }); - }); - - describe('getMigration is called', () => { - describe('when the school has a migration', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', - startedAt: new Date('2023-05-02'), - mandatorySince: new Date('2023-05-02'), - closedAt: new Date('2023-05-02'), - finishedAt: new Date('2023-05-02'), - }); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - schoolService.getSchoolById.mockResolvedValue(school); - authService.checkPermission.mockReturnValueOnce(); - }; - - it('should return a migration', async () => { - setup(); - - const result: OauthMigrationDto = await schoolUc.getMigration('schoolId', 'userId'); - - expect(result).toEqual({ - oauthMigrationPossible: undefined, - oauthMigrationMandatory: new Date('2023-05-02'), - oauthMigrationFinished: new Date('2023-05-02'), - oauthMigrationFinalFinish: new Date('2023-05-02'), - enableMigrationStart: true, - }); - }); - }); - - describe('when the school has no migration', () => { - const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); - schoolService.getSchoolById.mockResolvedValue(school); - authService.checkPermission.mockReturnValueOnce(); - }; - - it('should return no migration information', async () => { - setup(); - - const result: OauthMigrationDto = await schoolUc.getMigration('schoolId', 'userId'); - - expect(result).toEqual({ - enableMigrationStart: true, - }); - }); - }); - }); -}); diff --git a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts b/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts deleted file mode 100644 index 50fd7faf5a5..00000000000 --- a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { Permission, LegacySchoolDo, UserLoginMigrationDO, User } from '@shared/domain'; -import { - SchoolMigrationService, - UserLoginMigrationRevertService, - UserLoginMigrationService, -} from '@modules/user-login-migration'; -import { LegacySchoolService } from '../service'; -import { OauthMigrationDto } from './dto/oauth-migration.dto'; - -/** - * @deprecated because it uses the deprecated LegacySchoolDo. - */ -@Injectable() -export class LegacySchoolUc { - constructor( - private readonly schoolService: LegacySchoolService, - private readonly authService: AuthorizationService, - private readonly schoolMigrationService: SchoolMigrationService, - private readonly userLoginMigrationService: UserLoginMigrationService, - private readonly userLoginMigrationRevertService: UserLoginMigrationRevertService - ) {} - - // TODO: https://ticketsystem.dbildungscloud.de/browse/N21-673 Refactor this and split it up - async setMigration( - schoolId: string, - oauthMigrationPossible: boolean, - oauthMigrationMandatory: boolean, - oauthMigrationFinished: boolean, - userId: string - ): Promise { - const [authorizableUser, school]: [User, LegacySchoolDo] = await Promise.all([ - this.authService.getUserWithPermissions(userId), - this.schoolService.getSchoolById(schoolId), - ]); - - this.checkSchoolAuthorization(authorizableUser, school); - - const existingUserLoginMigration: UserLoginMigrationDO | null = - await this.userLoginMigrationService.findMigrationBySchool(schoolId); - - if (existingUserLoginMigration) { - this.schoolMigrationService.validateGracePeriod(existingUserLoginMigration); - } - - const updatedUserLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationService.setMigration( - schoolId, - oauthMigrationPossible, - oauthMigrationMandatory, - oauthMigrationFinished - ); - - if (!existingUserLoginMigration?.closedAt && updatedUserLoginMigration.closedAt) { - const hasSchoolMigratedUser = await this.schoolMigrationService.hasSchoolMigratedUser(schoolId); - - if (!hasSchoolMigratedUser) { - await this.userLoginMigrationRevertService.revertUserLoginMigration(updatedUserLoginMigration); - } else { - await this.schoolMigrationService.markUnmigratedUsersAsOutdated(schoolId); - } - } else if (existingUserLoginMigration?.closedAt && !updatedUserLoginMigration.closedAt) { - await this.schoolMigrationService.unmarkOutdatedUsers(schoolId); - } - - const migrationDto: OauthMigrationDto = new OauthMigrationDto({ - oauthMigrationPossible: !updatedUserLoginMigration.closedAt ? updatedUserLoginMigration.startedAt : undefined, - oauthMigrationMandatory: updatedUserLoginMigration.mandatorySince, - oauthMigrationFinished: updatedUserLoginMigration.closedAt, - oauthMigrationFinalFinish: updatedUserLoginMigration.finishedAt, - enableMigrationStart: !!school.officialSchoolNumber, - }); - - return migrationDto; - } - - async getMigration(schoolId: string, userId: string): Promise { - const [authorizableUser, school]: [User, LegacySchoolDo] = await Promise.all([ - this.authService.getUserWithPermissions(userId), - this.schoolService.getSchoolById(schoolId), - ]); - - this.checkSchoolAuthorization(authorizableUser, school); - - const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( - schoolId - ); - - const migrationDto: OauthMigrationDto = new OauthMigrationDto({ - oauthMigrationPossible: - userLoginMigration && !userLoginMigration.closedAt ? userLoginMigration.startedAt : undefined, - oauthMigrationMandatory: userLoginMigration ? userLoginMigration.mandatorySince : undefined, - oauthMigrationFinished: userLoginMigration ? userLoginMigration.closedAt : undefined, - oauthMigrationFinalFinish: userLoginMigration ? userLoginMigration.finishedAt : undefined, - enableMigrationStart: !!school.officialSchoolNumber, - }); - - return migrationDto; - } - - private checkSchoolAuthorization(authorizableUser: User, school: LegacySchoolDo): void { - const context = AuthorizationContextBuilder.read([Permission.SCHOOL_EDIT]); - this.authService.checkPermission(authorizableUser, school, context); - } -} diff --git a/apps/server/src/modules/oauth/controller/dto/index.ts b/apps/server/src/modules/oauth/controller/dto/index.ts index 9fc38145be5..7f679725bd3 100644 --- a/apps/server/src/modules/oauth/controller/dto/index.ts +++ b/apps/server/src/modules/oauth/controller/dto/index.ts @@ -1,4 +1 @@ export * from './authorization.params'; -export * from './system-id.params'; -export * from './sso-login.query'; -export * from './user-migration.response'; diff --git a/apps/server/src/modules/oauth/controller/dto/sso-login.query.ts b/apps/server/src/modules/oauth/controller/dto/sso-login.query.ts deleted file mode 100644 index 092380cbcf2..00000000000 --- a/apps/server/src/modules/oauth/controller/dto/sso-login.query.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsOptional, IsString } from 'class-validator'; - -export class SSOLoginQuery { - @IsOptional() - @IsString() - @ApiProperty() - postLoginRedirect?: string; - - @IsOptional() - @IsBoolean() - @ApiProperty() - migration?: boolean; -} diff --git a/apps/server/src/modules/oauth/controller/dto/system-id.params.ts b/apps/server/src/modules/oauth/controller/dto/system-id.params.ts deleted file mode 100644 index 04ba0017284..00000000000 --- a/apps/server/src/modules/oauth/controller/dto/system-id.params.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsMongoId } from 'class-validator'; - -export class SystemIdParams { - @IsMongoId() - @ApiProperty({ - description: 'The id of the system.', - required: true, - nullable: false, - }) - systemId!: string; -} diff --git a/apps/server/src/modules/oauth/controller/dto/user-migration.response.ts b/apps/server/src/modules/oauth/controller/dto/user-migration.response.ts deleted file mode 100644 index 88c0fdef3b4..00000000000 --- a/apps/server/src/modules/oauth/controller/dto/user-migration.response.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class UserMigrationResponse { - constructor(props: UserMigrationResponse) { - this.redirect = props.redirect; - } - - redirect: string; -} diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts index c42eeb22e24..e98100d3e43 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; +import { ICurrentUser } from '@modules/authentication'; +import { HydraOauthUc } from '@modules/oauth/uc/hydra-oauth.uc'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@modules/authentication'; -import { HydraOauthUc } from '@modules/oauth/uc/hydra-oauth.uc'; import { Request } from 'express'; -import { OauthSSOController } from './oauth-sso.controller'; import { StatelessAuthorizationParams } from './dto/stateless-authorization.params'; +import { OauthSSOController } from './oauth-sso.controller'; describe('OAuthController', () => { let module: TestingModule; diff --git a/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts b/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts deleted file mode 100644 index 42134d0b4d2..00000000000 --- a/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MigrationDto } from '@modules/user-login-migration/service/dto'; -import { UserMigrationResponse } from '../controller/dto'; - -export class UserMigrationMapper { - static mapDtoToResponse(dto: MigrationDto): UserMigrationResponse { - const response: UserMigrationResponse = new UserMigrationResponse({ - redirect: dto.redirect, - }); - - return response; - } -} diff --git a/apps/server/src/modules/oauth/oauth-api.module.ts b/apps/server/src/modules/oauth/oauth-api.module.ts index 2efacf66adf..880f11dc731 100644 --- a/apps/server/src/modules/oauth/oauth-api.module.ts +++ b/apps/server/src/modules/oauth/oauth-api.module.ts @@ -1,10 +1,3 @@ -import { AuthenticationModule } from '@modules/authentication/authentication.module'; -import { AuthorizationModule } from '@modules/authorization'; -import { LegacySchoolModule } from '@modules/legacy-school'; -import { ProvisioningModule } from '@modules/provisioning'; -import { SystemModule } from '@modules/system'; -import { UserModule } from '@modules/user'; -import { UserLoginMigrationModule } from '@modules/user-login-migration'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { OauthSSOController } from './controller/oauth-sso.controller'; @@ -12,17 +5,7 @@ import { OauthModule } from './oauth.module'; import { HydraOauthUc } from './uc'; @Module({ - imports: [ - OauthModule, - AuthenticationModule, - AuthorizationModule, - ProvisioningModule, - LegacySchoolModule, - UserLoginMigrationModule, - SystemModule, - UserModule, - LoggerModule, - ], + imports: [OauthModule, LoggerModule], controllers: [OauthSSOController], providers: [HydraOauthUc], }) diff --git a/apps/server/src/modules/oauth/oauth.module.ts b/apps/server/src/modules/oauth/oauth.module.ts index ae0f0eda48d..3898a2e8547 100644 --- a/apps/server/src/modules/oauth/oauth.module.ts +++ b/apps/server/src/modules/oauth/oauth.module.ts @@ -1,15 +1,15 @@ -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; import { CacheWrapperModule } from '@infra/cache'; import { EncryptionModule } from '@infra/encryption'; -import { LtiToolRepo } from '@shared/repo'; -import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '@modules/authorization'; -import { ProvisioningModule } from '@modules/provisioning'; import { LegacySchoolModule } from '@modules/legacy-school'; +import { ProvisioningModule } from '@modules/provisioning'; import { SystemModule } from '@modules/system'; import { UserModule } from '@modules/user'; import { UserLoginMigrationModule } from '@modules/user-login-migration'; +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { LtiToolRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; import { HydraSsoService } from './service/hydra.service'; import { OauthAdapterService } from './service/oauth-adapter.service'; import { OAuthService } from './service/oauth.service'; @@ -23,8 +23,8 @@ import { OAuthService } from './service/oauth.service'; UserModule, ProvisioningModule, SystemModule, - UserLoginMigrationModule, CacheWrapperModule, + UserLoginMigrationModule, LegacySchoolModule, ], providers: [OAuthService, OauthAdapterService, HydraSsoService, LtiToolRepo], diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 1ea2fe5ce07..adfa9603bc6 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -1,20 +1,20 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; -import { ProvisioningDto, ProvisioningService } from '@modules/provisioning'; -import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; +import { ProvisioningService } from '@modules/provisioning'; import { OauthConfigDto } from '@modules/system/service'; import { SystemDto } from '@modules/system/service/dto/system.dto'; import { SystemService } from '@modules/system/service/system.service'; import { UserService } from '@modules/user'; -import { MigrationCheckService, UserMigrationService } from '@modules/user-login-migration'; +import { MigrationCheckService } from '@modules/user-login-migration'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity } from '@shared/domain'; -import { UserDO } from '@shared/domain/domainobject/user.do'; +import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity, UserDO } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; +import { OauthDataDto } from '@src/modules/provisioning/dto'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { OAuthTokenDto } from '../interface'; import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; @@ -45,7 +45,6 @@ describe('OAuthService', () => { let provisioningService: DeepMocked; let userService: DeepMocked; let systemService: DeepMocked; - let userMigrationService: DeepMocked; let oauthAdapterService: DeepMocked; let migrationCheckService: DeepMocked; let schoolService: DeepMocked; @@ -85,10 +84,6 @@ describe('OAuthService', () => { provide: SystemService, useValue: createMock(), }, - { - provide: UserMigrationService, - useValue: createMock(), - }, { provide: OauthAdapterService, useValue: createMock(), @@ -105,7 +100,6 @@ describe('OAuthService', () => { provisioningService = module.get(ProvisioningService); userService = module.get(UserService); systemService = module.get(SystemService); - userMigrationService = module.get(UserMigrationService); oauthAdapterService = module.get(OauthAdapterService); migrationCheckService = module.get(MigrationCheckService); schoolService = module.get(LegacySchoolService); @@ -197,49 +191,6 @@ describe('OAuthService', () => { }); }); - describe('getPostLoginRedirectUrl is called', () => { - describe('when the oauth provider is iserv', () => { - it('should return an iserv login url string', async () => { - const system: SystemDto = new SystemDto({ - type: 'oauth', - oauthConfig: { - provider: 'iserv', - logoutEndpoint: 'http://iserv.de/logout', - } as OauthConfigDto, - }); - systemService.findById.mockResolvedValue(system); - - const result: string = await service.getPostLoginRedirectUrl('idToken', 'systemId'); - - expect(result).toEqual( - `http://iserv.de/logout?id_token_hint=idToken&post_logout_redirect_uri=https%3A%2F%2Fmock.de%2Fdashboard` - ); - }); - }); - - describe('when it is called with a postLoginRedirect and the provider is not iserv', () => { - it('should return the postLoginRedirect', async () => { - const system: SystemDto = new SystemDto({ type: 'oauth' }); - systemService.findById.mockResolvedValue(system); - - const result: string = await service.getPostLoginRedirectUrl('idToken', 'systemId', 'postLoginRedirect'); - - expect(result).toEqual('postLoginRedirect'); - }); - }); - - describe('when it is called with any other oauth provider', () => { - it('should return a login url string', async () => { - const system: SystemDto = new SystemDto({ type: 'oauth' }); - systemService.findById.mockResolvedValue(system); - - const result: string = await service.getPostLoginRedirectUrl('idToken', 'systemId'); - - expect(result).toEqual(`${hostUri}/dashboard`); - }); - }); - }); - describe('authenticateUser is called', () => { const setup = () => { const authCode = '43534543jnj543342jn2'; @@ -332,231 +283,489 @@ describe('OAuthService', () => { }); }); - describe('provisionUser is called', () => { - describe('when only provisioning a user', () => { - it('should return the user and a redirect', async () => { + describe('provisionUser', () => { + describe('when provisioning a user and a school without official school number', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; const externalUserId = 'externalUserId'; - const user: UserDO = userDoFactory.buildWithId({ externalId: externalUserId }); - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - const provisioningDto: ProvisioningDto = new ProvisioningDto({ - externalUserId, + + const user: UserDO = userDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalUserId, }); - provisioningService.getData.mockResolvedValue(oauthData); - provisioningService.provisionData.mockResolvedValue(provisioningDto); - userService.findByExternalId.mockResolvedValue(user); + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { + externalId: externalUserId, + }, + externalSchool: { + externalId: 'externalSchoolId', + name: 'External School', + }, + }); - const result: { user?: UserDO; redirect: string } = await service.provisionUser( - 'systemId', - 'idToken', - 'accessToken' - ); + provisioningService.getData.mockResolvedValueOnce(provisioningData); + userService.findByExternalId.mockResolvedValueOnce(user); - expect(result).toEqual<{ user?: UserDO; redirect: string }>({ + return { + systemId, + idToken, + accessToken, + provisioningData, user, - redirect: `${hostUri}/dashboard`, - }); + }; + }; + + it('should provision the data', async () => { + const { systemId, idToken, accessToken, provisioningData } = setup(); + + await service.provisionUser(systemId, idToken, accessToken); + + expect(provisioningService.provisionData).toHaveBeenCalledWith(provisioningData); + }); + + it('should return the user', async () => { + const { systemId, idToken, accessToken, user } = setup(); + + const result = await service.provisionUser(systemId, idToken, accessToken); + + expect(result).toEqual(user); }); }); - describe('when provisioning a user that should migrate, but the user does not exist', () => { - it('should return a redirect to the migration page and skip provisioning', async () => { - const migrationRedirectUrl = 'migrationRedirectUrl'; - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), - externalSchool: new ExternalSchoolDto({ - externalId: 'schoolExternalId', - name: 'externalSchool', - officialSchoolNumber: 'officialSchoolNumber', - }), + describe('when provisioning a user and a school without official school number, but the user cannot be found after the provisioning', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; + const externalUserId = 'externalUserId'; + + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { + externalId: externalUserId, + }, + externalSchool: { + externalId: 'externalSchoolId', + name: 'External School', + }, }); - provisioningService.getData.mockResolvedValue(oauthData); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(null); - migrationCheckService.shouldUserMigrate.mockResolvedValue(true); - userMigrationService.getMigrationConsentPageRedirect.mockResolvedValue(migrationRedirectUrl); - userService.findByExternalId.mockResolvedValue(null); + provisioningService.getData.mockResolvedValueOnce(provisioningData); + userService.findByExternalId.mockResolvedValueOnce(null); - const result: { user?: UserDO; redirect: string } = await service.provisionUser( - 'systemId', - 'idToken', - 'accessToken' - ); + return { + systemId, + idToken, + accessToken, + provisioningData, + }; + }; - expect(result).toEqual<{ user?: UserDO; redirect: string }>({ - user: undefined, - redirect: migrationRedirectUrl, - }); - expect(provisioningService.provisionData).not.toHaveBeenCalled(); + it('should throw an error', async () => { + const { systemId, idToken, accessToken } = setup(); + + await expect(service.provisionUser(systemId, idToken, accessToken)).rejects.toThrow( + UserNotFoundAfterProvisioningLoggableException + ); }); }); - describe('when provisioning an existing user that should migrate', () => { - it('should return a redirect to the migration page and provision the user', async () => { - const migrationRedirectUrl = 'migrationRedirectUrl'; + describe('when provisioning a user and a new school with official school number', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; const externalUserId = 'externalUserId'; - const user: UserDO = userDoFactory.buildWithId({ externalId: externalUserId }); - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalUser: new ExternalUserDto({ + + const user: UserDO = userDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalUserId, + }); + + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { externalId: externalUserId, - }), - externalSchool: new ExternalSchoolDto({ - externalId: 'schoolExternalId', - name: 'externalSchool', + }, + externalSchool: { + externalId: 'externalSchoolId', + name: 'External School', officialSchoolNumber: 'officialSchoolNumber', - }), + }, + }); + + provisioningService.getData.mockResolvedValueOnce(provisioningData); + schoolService.getSchoolBySchoolNumber.mockResolvedValueOnce(null); + migrationCheckService.shouldUserMigrate.mockResolvedValueOnce(false); + userService.findByExternalId.mockResolvedValueOnce(user); + + return { + systemId, + idToken, + accessToken, + provisioningData, + user, + }; + }; + + it('should provision the data', async () => { + const { systemId, idToken, accessToken, provisioningData } = setup(); + + await service.provisionUser(systemId, idToken, accessToken); + + expect(provisioningService.provisionData).toHaveBeenCalledWith(provisioningData); + }); + + it('should return the user', async () => { + const { systemId, idToken, accessToken, user } = setup(); + + const result = await service.provisionUser(systemId, idToken, accessToken); + + expect(result).toEqual(user); + }); + }); + + describe('when provisioning a user and an existing school with official school number, that has provisioning enabled', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const officialSchoolNumber = 'officialSchoolNumber'; + + const user: UserDO = userDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalUserId, }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + + const school: LegacySchoolDo = legacySchoolDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalSchoolId, + officialSchoolNumber, features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }); - provisioningService.getData.mockResolvedValue(oauthData); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); - migrationCheckService.shouldUserMigrate.mockResolvedValue(true); - userMigrationService.getMigrationConsentPageRedirect.mockResolvedValue(migrationRedirectUrl); - userService.findByExternalId.mockResolvedValue(user); + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { + externalId: externalUserId, + }, + externalSchool: { + externalId: externalSchoolId, + name: school.name, + officialSchoolNumber, + }, + }); - const result: { user?: UserDO; redirect: string } = await service.provisionUser( - 'systemId', - 'idToken', - 'accessToken' - ); + provisioningService.getData.mockResolvedValueOnce(provisioningData); + schoolService.getSchoolBySchoolNumber.mockResolvedValueOnce(school); + migrationCheckService.shouldUserMigrate.mockResolvedValueOnce(false); + userService.findByExternalId.mockResolvedValueOnce(user); - expect(result).toEqual<{ user?: UserDO; redirect: string }>({ + return { + systemId, + idToken, + accessToken, + provisioningData, user, - redirect: migrationRedirectUrl, - }); - expect(provisioningService.provisionData).toHaveBeenCalled(); + }; + }; + + it('should provision the data', async () => { + const { systemId, idToken, accessToken, provisioningData } = setup(); + + await service.provisionUser(systemId, idToken, accessToken); + + expect(provisioningService.provisionData).toHaveBeenCalledWith(provisioningData); + }); + + it('should return the user', async () => { + const { systemId, idToken, accessToken, user } = setup(); + + const result = await service.provisionUser(systemId, idToken, accessToken); + + expect(result).toEqual(user); }); }); - describe('when provisioning an existing user, that is in a school with provisioning disabled', () => { + describe('when provisioning an existing user and an existing school with official school number, that has provisioning disabled', () => { const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; const externalUserId = 'externalUserId'; - const user: UserDO = userDoFactory.buildWithId({ externalId: externalUserId }); - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalUser: new ExternalUserDto({ + const externalSchoolId = 'externalSchoolId'; + const officialSchoolNumber = 'officialSchoolNumber'; + + const user: UserDO = userDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalUserId, + }); + + const school: LegacySchoolDo = legacySchoolDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalSchoolId, + officialSchoolNumber, + features: [], + }); + + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { externalId: externalUserId, - }), - externalSchool: new ExternalSchoolDto({ - externalId: 'schoolExternalId', - name: 'externalSchool', - officialSchoolNumber: 'officialSchoolNumber', - }), + }, + externalSchool: { + externalId: externalSchoolId, + name: school.name, + officialSchoolNumber, + }, }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [] }); - provisioningService.getData.mockResolvedValue(oauthData); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); - migrationCheckService.shouldUserMigrate.mockResolvedValue(false); - userService.findByExternalId.mockResolvedValue(user); + provisioningService.getData.mockResolvedValueOnce(provisioningData); + schoolService.getSchoolBySchoolNumber.mockResolvedValueOnce(school); + migrationCheckService.shouldUserMigrate.mockResolvedValueOnce(false); + userService.findByExternalId.mockResolvedValueOnce(user); return { + systemId, + idToken, + accessToken, + provisioningData, user, }; }; - it('should not provision the user, but find it', async () => { - const { user } = setup(); + it('should not provision the data', async () => { + const { systemId, idToken, accessToken } = setup(); - const result: { user?: UserDO; redirect: string } = await service.provisionUser( - 'systemId', - 'idToken', - 'accessToken' - ); + await service.provisionUser(systemId, idToken, accessToken); - expect(result).toEqual<{ user?: UserDO; redirect: string }>({ - user, - redirect: 'https://mock.de/dashboard', - }); expect(provisioningService.provisionData).not.toHaveBeenCalled(); }); + + it('should return the user', async () => { + const { systemId, idToken, accessToken, user } = setup(); + + const result = await service.provisionUser(systemId, idToken, accessToken); + + expect(result).toEqual(user); + }); }); - describe('when provisioning a new user, that is in a school with provisioning disabled', () => { + describe('when provisioning a new user and an existing school with official school number, that has provisioning disabled', () => { const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; const externalUserId = 'externalUserId'; - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalUser: new ExternalUserDto({ + const externalSchoolId = 'externalSchoolId'; + const officialSchoolNumber = 'officialSchoolNumber'; + + const school: LegacySchoolDo = legacySchoolDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalSchoolId, + officialSchoolNumber, + features: [], + }); + + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { externalId: externalUserId, - }), - externalSchool: new ExternalSchoolDto({ - externalId: 'schoolExternalId', - name: 'externalSchool', - officialSchoolNumber: 'officialSchoolNumber', - }), + }, + externalSchool: { + externalId: externalSchoolId, + name: school.name, + officialSchoolNumber, + }, }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [] }); - provisioningService.getData.mockResolvedValue(oauthData); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); - migrationCheckService.shouldUserMigrate.mockResolvedValue(false); - userService.findByExternalId.mockResolvedValue(null); + provisioningService.getData.mockResolvedValueOnce(provisioningData); + schoolService.getSchoolBySchoolNumber.mockResolvedValueOnce(school); + migrationCheckService.shouldUserMigrate.mockResolvedValueOnce(false); + userService.findByExternalId.mockResolvedValueOnce(null); + + return { + systemId, + idToken, + accessToken, + provisioningData, + }; }; - it('should throw UserNotFoundAfterProvisioningLoggableException', async () => { - setup(); + it('should not provision the data', async () => { + const { systemId, idToken, accessToken } = setup(); - const func = () => service.provisionUser('systemId', 'idToken', 'accessToken'); + await expect(service.provisionUser(systemId, idToken, accessToken)).rejects.toThrow(); - await expect(func).rejects.toThrow(UserNotFoundAfterProvisioningLoggableException); expect(provisioningService.provisionData).not.toHaveBeenCalled(); }); + + it('should throw an error', async () => { + const { systemId, idToken, accessToken } = setup(); + + await expect(service.provisionUser(systemId, idToken, accessToken)).rejects.toThrow( + UserNotFoundAfterProvisioningLoggableException + ); + }); }); - describe('when the user cannot be found after provisioning', () => { + describe('when provisioning a new user and an existing school with official school number, that is currently migrating', () => { const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; const externalUserId = 'externalUserId'; - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalUser: new ExternalUserDto({ + const externalSchoolId = 'externalSchoolId'; + const officialSchoolNumber = 'officialSchoolNumber'; + + const school: LegacySchoolDo = legacySchoolDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalSchoolId, + officialSchoolNumber, + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); + + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { externalId: externalUserId, - }), + }, + externalSchool: { + externalId: externalSchoolId, + name: school.name, + officialSchoolNumber, + }, + }); + + provisioningService.getData.mockResolvedValueOnce(provisioningData); + schoolService.getSchoolBySchoolNumber.mockResolvedValueOnce(school); + migrationCheckService.shouldUserMigrate.mockResolvedValueOnce(true); + userService.findByExternalId.mockResolvedValueOnce(null); + + return { + systemId, + idToken, + accessToken, + provisioningData, + }; + }; + + it('should not provision the data', async () => { + const { systemId, idToken, accessToken } = setup(); + + await service.provisionUser(systemId, idToken, accessToken); + + expect(provisioningService.provisionData).not.toHaveBeenCalled(); + }); + + it('should return null', async () => { + const { systemId, idToken, accessToken } = setup(); + + const result = await service.provisionUser(systemId, idToken, accessToken); + + expect(result).toBeNull(); + }); + }); + + describe('when provisioning an existing user and an existing school with official school number, that is currently migrating', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const idToken = 'idToken'; + const accessToken = 'accessToken'; + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const officialSchoolNumber = 'officialSchoolNumber'; + + const user: UserDO = userDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalUserId, }); - const provisioningDto: ProvisioningDto = new ProvisioningDto({ - externalUserId, + + const school: LegacySchoolDo = legacySchoolDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: externalSchoolId, + officialSchoolNumber, + features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], + }); + + const provisioningData: OauthDataDto = new OauthDataDto({ + system: { + systemId, + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: 'https://mock.person-info.de/', + }, + externalUser: { + externalId: externalUserId, + }, + externalSchool: { + externalId: externalSchoolId, + name: school.name, + officialSchoolNumber, + }, }); - provisioningService.getData.mockResolvedValue(oauthData); - provisioningService.provisionData.mockResolvedValue(provisioningDto); - userService.findByExternalId.mockResolvedValue(null); + provisioningService.getData.mockResolvedValueOnce(provisioningData); + schoolService.getSchoolBySchoolNumber.mockResolvedValueOnce(school); + migrationCheckService.shouldUserMigrate.mockResolvedValueOnce(true); + userService.findByExternalId.mockResolvedValueOnce(user).mockResolvedValueOnce(user); + + return { + systemId, + idToken, + accessToken, + provisioningData, + user, + }; }; - it('should throw an error', async () => { - setup(); + it('should provision the data', async () => { + const { systemId, idToken, accessToken, provisioningData } = setup(); + + await service.provisionUser(systemId, idToken, accessToken); + + expect(provisioningService.provisionData).toHaveBeenCalledWith(provisioningData); + }); + + it('should return the user', async () => { + const { systemId, idToken, accessToken, user } = setup(); - const func = () => service.provisionUser('systemId', 'idToken', 'accessToken'); + const result = await service.provisionUser(systemId, idToken, accessToken); - await expect(func).rejects.toThrow(UserNotFoundAfterProvisioningLoggableException); + expect(result).toEqual(user); }); }); }); diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 1f7935d919e..9a484a52d47 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,19 +1,18 @@ -import { Configuration } from '@hpi-schul-cloud/commons'; -import { Inject } from '@nestjs/common'; -import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { EntityId, LegacySchoolDo, OauthConfig, SchoolFeatures, UserDO } from '@shared/domain'; import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; -import { LegacyLogger } from '@src/core/logger'; +import { LegacySchoolService } from '@modules/legacy-school'; import { ProvisioningService } from '@modules/provisioning'; import { OauthDataDto } from '@modules/provisioning/dto'; -import { LegacySchoolService } from '@modules/legacy-school'; import { SystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; -import { MigrationCheckService, UserMigrationService } from '@modules/user-login-migration'; +import { MigrationCheckService } from '@modules/user-login-migration'; +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { EntityId, LegacySchoolDo, OauthConfig, SchoolFeatures, UserDO } from '@shared/domain'; +import { LegacyLogger } from '@src/core/logger'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { OAuthSSOError, SSOErrorCode, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { OAuthTokenDto } from '../interface'; +import { OAuthSSOError, SSOErrorCode, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { TokenRequestMapper } from '../mapper/token-request.mapper'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; @@ -27,7 +26,6 @@ export class OAuthService { private readonly logger: LegacyLogger, private readonly provisioningService: ProvisioningService, private readonly systemService: SystemService, - private readonly userMigrationService: UserMigrationService, private readonly migrationCheckService: MigrationCheckService, private readonly schoolService: LegacySchoolService ) { @@ -60,22 +58,16 @@ export class OAuthService { return oauthTokens; } - async provisionUser( - systemId: string, - idToken: string, - accessToken: string, - postLoginRedirect?: string - ): Promise<{ user?: UserDO; redirect: string }> { + async provisionUser(systemId: string, idToken: string, accessToken: string): Promise { const data: OauthDataDto = await this.provisioningService.getData(systemId, idToken, accessToken); const externalUserId: string = data.externalUser.externalId; const officialSchoolNumber: string | undefined = data.externalSchool?.officialSchoolNumber; - let provisioning = true; - let migrationConsentRedirect: string | undefined; + let isProvisioningEnabled = true; if (officialSchoolNumber) { - provisioning = await this.isOauthProvisioningEnabledForSchool(officialSchoolNumber); + isProvisioningEnabled = await this.isOauthProvisioningEnabledForSchool(officialSchoolNumber); const shouldUserMigrate: boolean = await this.migrationCheckService.shouldUserMigrate( externalUserId, @@ -84,33 +76,21 @@ export class OAuthService { ); if (shouldUserMigrate) { - // TODO: https://ticketsystem.dbildungscloud.de/browse/N21-632 Move Redirect Logic URLs to Client - migrationConsentRedirect = await this.userMigrationService.getMigrationConsentPageRedirect( - officialSchoolNumber, - systemId - ); - const existingUser: UserDO | null = await this.userService.findByExternalId(externalUserId, systemId); + if (!existingUser) { - return { user: undefined, redirect: migrationConsentRedirect }; + return null; } } } - if (provisioning) { + if (isProvisioningEnabled) { await this.provisioningService.provisionData(data); } const user: UserDO = await this.findUserAfterProvisioningOrThrow(externalUserId, systemId, officialSchoolNumber); - // TODO: https://ticketsystem.dbildungscloud.de/browse/N21-632 Move Redirect Logic URLs to Client - const redirect: string = await this.getPostLoginRedirectUrl( - idToken, - systemId, - postLoginRedirect || migrationConsentRedirect - ); - - return { user, redirect }; + return user; } private async findUserAfterProvisioningOrThrow( @@ -166,26 +146,6 @@ export class OAuthService { return decodedJWT; } - async getPostLoginRedirectUrl(idToken: string, systemId: string, postLoginRedirect?: string): Promise { - const clientUrl: string = Configuration.get('HOST') as string; - const dashboardUrl: URL = new URL('/dashboard', clientUrl); - const system: SystemDto = await this.systemService.findById(systemId); - - let redirect: string; - if (system.oauthConfig?.provider === 'iserv' && system.oauthConfig?.logoutEndpoint) { - const iservLogoutUrl: URL = new URL(system.oauthConfig.logoutEndpoint); - iservLogoutUrl.searchParams.append('id_token_hint', idToken); - iservLogoutUrl.searchParams.append('post_logout_redirect_uri', postLoginRedirect || dashboardUrl.toString()); - redirect = iservLogoutUrl.toString(); - } else if (postLoginRedirect) { - redirect = postLoginRedirect; - } else { - redirect = dashboardUrl.toString(); - } - - return redirect; - } - private buildTokenRequestPayload( code: string, oauthConfig: OauthConfig, diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index a812ff773b2..c048156d08b 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -1,4 +1,8 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; +import { MailModule } from '@infra/mail'; +import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@infra/rabbitmq'; +import { REDIS_CLIENT, RedisModule } from '@infra/redis'; import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { AccountApiModule } from '@modules/account/account-api.module'; @@ -8,7 +12,6 @@ import { CollaborativeStorageModule } from '@modules/collaborative-storage'; import { FilesStorageClientModule } from '@modules/files-storage-client'; import { GroupApiModule } from '@modules/group/group-api.module'; import { LearnroomApiModule } from '@modules/learnroom/learnroom-api.module'; -import { LegacySchoolApiModule } from '@modules/legacy-school/legacy-school-api.module'; import { LessonApiModule } from '@modules/lesson/lesson-api.module'; import { MetaTagExtractorApiModule, MetaTagExtractorModule } from '@modules/meta-tag-extractor'; import { NewsModule } from '@modules/news'; @@ -28,10 +31,6 @@ import { VideoConferenceApiModule } from '@modules/video-conference/video-confer import { DynamicModule, Inject, MiddlewareConsumer, Module, NestModule, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain'; -import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; -import { MailModule } from '@infra/mail'; -import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@infra/rabbitmq'; -import { RedisModule, REDIS_CLIENT } from '@infra/redis'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LegacyLogger, LoggerModule } from '@src/core/logger'; @@ -68,7 +67,6 @@ const serverModules = [ adminUser: Configuration.get('ROCKET_CHAT_ADMIN_USER') as string, adminPassword: Configuration.get('ROCKET_CHAT_ADMIN_PASSWORD') as string, }), - LegacySchoolApiModule, VideoConferenceApiModule, OauthProviderApiModule, SharingApiModule, diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts index 154148c1fa0..f23795a35ab 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts @@ -688,7 +688,11 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - await em.persistAndFlush([teacherAccount, teacherUser]); + const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ + school: teacherUser.school, + }); + + await em.persistAndFlush([teacherAccount, teacherUser, userLoginMigration]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); @@ -1156,9 +1160,22 @@ describe('UserLoginMigrationController (API)', () => { closedAt: new Date(2023, 1, 5), }); + const migratedUser = userFactory.buildWithId({ + school, + lastLoginSystemChange: new Date(2023, 1, 4), + }); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); - await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); + await em.persistAndFlush([ + sourceSystem, + targetSystem, + school, + adminAccount, + adminUser, + userLoginMigration, + migratedUser, + ]); em.clear(); const loggedInClient = await testApiClient.login(adminAccount); diff --git a/apps/server/src/modules/user-login-migration/controller/dto/index.ts b/apps/server/src/modules/user-login-migration/controller/dto/index.ts index a158f06bb75..bcbb7e46f4e 100644 --- a/apps/server/src/modules/user-login-migration/controller/dto/index.ts +++ b/apps/server/src/modules/user-login-migration/controller/dto/index.ts @@ -1,5 +1,3 @@ export * from './request/user-login-migration-search.params'; -export * from './request/page-type.query.param'; export * from './response/user-login-migration.response'; export * from './response/user-login-migration-search-list.response'; -export * from './response/page-content.response'; diff --git a/apps/server/src/modules/user-login-migration/controller/dto/request/page-type.query.param.ts b/apps/server/src/modules/user-login-migration/controller/dto/request/page-type.query.param.ts deleted file mode 100644 index 6c755052944..00000000000 --- a/apps/server/src/modules/user-login-migration/controller/dto/request/page-type.query.param.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IsEnum, IsMongoId } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { PageTypes } from '../../../interface/page-types.enum'; - -export class PageContentQueryParams { - @ApiProperty({ description: 'The Type of Page that is displayed', type: PageTypes }) - @IsEnum(PageTypes) - pageType!: PageTypes; - - @ApiProperty({ description: 'The Source System' }) - @IsMongoId() - sourceSystem!: string; - - @ApiProperty({ description: 'The Target System' }) - @IsMongoId() - targetSystem!: string; -} diff --git a/apps/server/src/modules/user-login-migration/controller/dto/response/page-content.response.ts b/apps/server/src/modules/user-login-migration/controller/dto/response/page-content.response.ts deleted file mode 100644 index 836412b64c7..00000000000 --- a/apps/server/src/modules/user-login-migration/controller/dto/response/page-content.response.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class PageContentResponse { - @ApiProperty({ - description: 'The URL for the proceed button', - }) - proceedButtonUrl: string; - - @ApiProperty({ - description: 'The URL for the cancel button', - }) - cancelButtonUrl: string; - - constructor(props: PageContentResponse) { - this.proceedButtonUrl = props.proceedButtonUrl; - this.cancelButtonUrl = props.cancelButtonUrl; - } -} diff --git a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts index fc1c9c9f9cf..5489e33e3a4 100644 --- a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts +++ b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts @@ -17,7 +17,7 @@ import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, -} from '../error'; +} from '../loggable'; import { UserLoginMigrationMapper } from '../mapper'; import { CloseUserLoginMigrationUc, diff --git a/apps/server/src/modules/user-login-migration/controller/user-migration.controller.spec.ts b/apps/server/src/modules/user-login-migration/controller/user-migration.controller.spec.ts deleted file mode 100644 index 5307fa5ac2f..00000000000 --- a/apps/server/src/modules/user-login-migration/controller/user-migration.controller.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { PageTypes } from '../interface/page-types.enum'; -import { PageContentMapper } from '../mapper'; -import { PageContentDto } from '../service/dto'; -import { UserLoginMigrationUc } from '../uc/user-login-migration.uc'; -import { PageContentQueryParams, PageContentResponse } from './dto'; -import { UserMigrationController } from './user-migration.controller'; - -describe('MigrationController', () => { - let module: TestingModule; - let controller: UserMigrationController; - let uc: DeepMocked; - let mapper: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - { - provide: UserLoginMigrationUc, - useValue: createMock(), - }, - { - provide: PageContentMapper, - useValue: createMock(), - }, - ], - controllers: [UserMigrationController], - }).compile(); - - controller = module.get(UserMigrationController); - uc = module.get(UserLoginMigrationUc); - mapper = module.get(PageContentMapper); - }); - afterAll(async () => { - await module.close(); - }); - - const setup = () => { - const query: PageContentQueryParams = { - pageType: PageTypes.START_FROM_TARGET_SYSTEM, - sourceSystem: 'source', - targetSystem: 'target', - }; - const dto: PageContentDto = new PageContentDto({ - proceedButtonUrl: 'proceedUrl', - cancelButtonUrl: 'cancelUrl', - }); - const response: PageContentResponse = new PageContentResponse({ - proceedButtonUrl: 'proceedUrl', - cancelButtonUrl: 'cancelUrl', - }); - return { query, dto, response }; - }; - - describe('getMigrationPageDetails is called', () => { - describe('when pagecontent is requested', () => { - it('should return a response', async () => { - const { query, dto, response } = setup(); - mapper.mapDtoToResponse.mockReturnValue(response); - uc.getPageContent.mockResolvedValue(dto); - const testResp: PageContentResponse = await controller.getMigrationPageDetails(query); - expect(testResp).toEqual(response); - }); - }); - }); -}); diff --git a/apps/server/src/modules/user-login-migration/controller/user-migration.controller.ts b/apps/server/src/modules/user-login-migration/controller/user-migration.controller.ts deleted file mode 100644 index c3afc626ed9..00000000000 --- a/apps/server/src/modules/user-login-migration/controller/user-migration.controller.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Controller, Get, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { PageContentMapper } from '../mapper'; -import { PageContentDto } from '../service/dto'; -import { UserLoginMigrationUc } from '../uc/user-login-migration.uc'; -import { PageContentQueryParams, PageContentResponse } from './dto'; - -@ApiTags('UserMigration') -@Controller('user-migration') -/** - * @Deprecated - */ -export class UserMigrationController { - constructor(private readonly uc: UserLoginMigrationUc, private readonly pageContentMapper: PageContentMapper) {} - - @Get('page-content') - async getMigrationPageDetails(@Query() pageTypeQuery: PageContentQueryParams): Promise { - const content: PageContentDto = await this.uc.getPageContent( - pageTypeQuery.pageType, - pageTypeQuery.sourceSystem, - pageTypeQuery.targetSystem - ); - - const response: PageContentResponse = this.pageContentMapper.mapDtoToResponse(content); - return response; - } -} diff --git a/apps/server/src/modules/user-login-migration/error/index.ts b/apps/server/src/modules/user-login-migration/error/index.ts deleted file mode 100644 index e5ac9f5f970..00000000000 --- a/apps/server/src/modules/user-login-migration/error/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './oauth-migration.error'; -export * from './school-migration.error'; -export * from './user-login-migration.error'; -export * from './school-number-missing.loggable-exception'; -export * from './user-login-migration-already-closed.loggable-exception'; -export * from './user-login-migration-grace-period-expired-loggable.exception'; -export * from './user-login-migration-not-found.loggable-exception'; diff --git a/apps/server/src/modules/user-login-migration/error/oauth-migration.error.spec.ts b/apps/server/src/modules/user-login-migration/error/oauth-migration.error.spec.ts deleted file mode 100644 index d42801c4287..00000000000 --- a/apps/server/src/modules/user-login-migration/error/oauth-migration.error.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { OAuthMigrationError } from './oauth-migration.error'; - -describe('Oauth Migration Error', () => { - it('should be possible to create', () => { - const error = new OAuthMigrationError(); - expect(error).toBeDefined(); - expect(error.message).toEqual(error.DEFAULT_MESSAGE); - expect(error.errorcode).toEqual(error.DEFAULT_ERRORCODE); - }); - - it('should be possible to add message', () => { - const msg = 'test message'; - const error = new OAuthMigrationError(msg); - expect(error.message).toEqual(msg); - }); - - it('should have the right code', () => { - const errCode = 'test_code'; - const error = new OAuthMigrationError('', errCode); - expect(error.errorcode).toEqual(errCode); - }); - - it('should create with specific parameter', () => { - const error = new OAuthMigrationError(undefined, undefined, '12345', '11111'); - expect(error).toBeDefined(); - expect(error.message).toEqual(error.DEFAULT_MESSAGE); - expect(error.errorcode).toEqual(error.DEFAULT_ERRORCODE); - expect(error.officialSchoolNumberFromSource).toEqual('12345'); - expect(error.officialSchoolNumberFromTarget).toEqual('11111'); - }); -}); diff --git a/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts b/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts deleted file mode 100644 index c21185f1c93..00000000000 --- a/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { OAuthSSOError } from '@modules/oauth/loggable'; - -export class OAuthMigrationError extends OAuthSSOError { - readonly message: string; - - readonly errorcode: string; - - readonly DEFAULT_MESSAGE: string = 'Error in Oauth Migration Process.'; - - readonly DEFAULT_ERRORCODE: string = 'OauthMigrationFailed'; - - readonly officialSchoolNumberFromSource?: string; - - readonly officialSchoolNumberFromTarget?: string; - - constructor( - message?: string, - errorcode?: string, - officialSchoolNumberFromSource?: string, - officialSchoolNumberFromTarget?: string - ) { - super(message); - this.message = message || this.DEFAULT_MESSAGE; - this.errorcode = errorcode || this.DEFAULT_ERRORCODE; - this.officialSchoolNumberFromSource = officialSchoolNumberFromSource; - this.officialSchoolNumberFromTarget = officialSchoolNumberFromTarget; - } -} diff --git a/apps/server/src/modules/user-login-migration/error/school-migration.error.ts b/apps/server/src/modules/user-login-migration/error/school-migration.error.ts deleted file mode 100644 index 3887b693f49..00000000000 --- a/apps/server/src/modules/user-login-migration/error/school-migration.error.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HttpStatus } from '@nestjs/common'; -import { BusinessError } from '@shared/common'; - -export class SchoolMigrationError extends BusinessError { - constructor(details?: Record, cause?: unknown) { - super( - { - type: 'SCHOOL_MIGRATION_FAILED', - title: 'Migration of school failed.', - defaultMessage: 'School could not migrate during user migration process.', - }, - HttpStatus.INTERNAL_SERVER_ERROR, - details, - cause - ); - } -} diff --git a/apps/server/src/modules/user-login-migration/error/user-login-migration.error.ts b/apps/server/src/modules/user-login-migration/error/user-login-migration.error.ts deleted file mode 100644 index 29239fc817c..00000000000 --- a/apps/server/src/modules/user-login-migration/error/user-login-migration.error.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HttpStatus } from '@nestjs/common'; -import { BusinessError } from '@shared/common'; - -export class UserLoginMigrationError extends BusinessError { - constructor(details?: Record) { - super( - { - type: 'USER_MIGRATION_FAILED', - title: 'Migration failed', - defaultMessage: 'Migration of user failed during migration process', - }, - HttpStatus.INTERNAL_SERVER_ERROR, - details - ); - } -} diff --git a/apps/server/src/modules/user-login-migration/index.ts b/apps/server/src/modules/user-login-migration/index.ts index c357ceaf98e..c3841ebf249 100644 --- a/apps/server/src/modules/user-login-migration/index.ts +++ b/apps/server/src/modules/user-login-migration/index.ts @@ -1,3 +1,2 @@ export * from './user-login-migration.module'; export * from './service'; -export * from './error'; diff --git a/apps/server/src/modules/user-login-migration/interface/page-types.enum.ts b/apps/server/src/modules/user-login-migration/interface/page-types.enum.ts deleted file mode 100644 index 87848b71e31..00000000000 --- a/apps/server/src/modules/user-login-migration/interface/page-types.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum PageTypes { - START_FROM_TARGET_SYSTEM = 'start_from_target_system', - START_FROM_SOURCE_SYSTEM = 'start_from_source_system', - START_FROM_SOURCE_SYSTEM_MANDATORY = 'start_from_source_system_mandatory', -} diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/index.ts b/apps/server/src/modules/user-login-migration/loggable/debug/index.ts new file mode 100644 index 00000000000..cf5d1274646 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/index.ts @@ -0,0 +1,3 @@ +export * from './school-migration-successful.loggable'; +export * from './user-migration-started.loggable'; +export * from './user-migration-successful.loggable'; diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.spec.ts new file mode 100644 index 00000000000..d5e8d7407f6 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.spec.ts @@ -0,0 +1,42 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { legacySchoolDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { SchoolMigrationSuccessfulLoggable } from './school-migration-successful.loggable'; + +describe(SchoolMigrationSuccessfulLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const school = legacySchoolDoFactory.build({ + id: new ObjectId().toHexString(), + externalId: 'externalId', + previousExternalId: 'previousExternalId', + }); + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + }); + + const exception = new SchoolMigrationSuccessfulLoggable(school, userLoginMigration); + + return { + exception, + school, + userLoginMigration, + }; + }; + + it('should return the correct log message', () => { + const { exception, school, userLoginMigration } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'A school has successfully migrated.', + data: { + schoolId: school.id, + externalId: school.externalId, + previousExternalId: school.previousExternalId, + userLoginMigrationId: userLoginMigration.id, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.ts new file mode 100644 index 00000000000..2614ae24c72 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/school-migration-successful.loggable.ts @@ -0,0 +1,18 @@ +import { LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain'; +import { Loggable, LogMessage } from '@src/core/logger'; + +export class SchoolMigrationSuccessfulLoggable implements Loggable { + constructor(private readonly school: LegacySchoolDo, private readonly userLoginMigration: UserLoginMigrationDO) {} + + getLogMessage(): LogMessage { + return { + message: 'A school has successfully migrated.', + data: { + schoolId: this.school.id, + externalId: this.school.externalId, + previousExternalId: this.school.previousExternalId, + userLoginMigrationId: this.userLoginMigration.id, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.spec.ts new file mode 100644 index 00000000000..22c2ded1b67 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.spec.ts @@ -0,0 +1,34 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { userLoginMigrationDOFactory } from '@shared/testing'; +import { UserMigrationStartedLoggable } from './user-migration-started.loggable'; + +describe(UserMigrationStartedLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId(); + + const exception = new UserMigrationStartedLoggable(userId, userLoginMigration); + + return { + exception, + userId, + userLoginMigration, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, userLoginMigration } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'A user started the user login migration.', + data: { + userId, + userLoginMigrationId: userLoginMigration.id, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.ts new file mode 100644 index 00000000000..49c2b8c4813 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-started.loggable.ts @@ -0,0 +1,16 @@ +import { EntityId, UserLoginMigrationDO } from '@shared/domain'; +import { Loggable, LogMessage } from '@src/core/logger'; + +export class UserMigrationStartedLoggable implements Loggable { + constructor(private readonly userId: EntityId, private readonly userLoginMigration: UserLoginMigrationDO) {} + + getLogMessage(): LogMessage { + return { + message: 'A user started the user login migration.', + data: { + userId: this.userId, + userLoginMigrationId: this.userLoginMigration.id, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.spec.ts new file mode 100644 index 00000000000..aae8baf96d9 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.spec.ts @@ -0,0 +1,34 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { userLoginMigrationDOFactory } from '@shared/testing'; +import { UserMigrationSuccessfulLoggable } from './user-migration-successful.loggable'; + +describe(UserMigrationSuccessfulLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId(); + + const exception = new UserMigrationSuccessfulLoggable(userId, userLoginMigration); + + return { + exception, + userId, + userLoginMigration, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, userLoginMigration } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'A user has successfully migrated.', + data: { + userId, + userLoginMigrationId: userLoginMigration.id, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.ts new file mode 100644 index 00000000000..d61259816c5 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/debug/user-migration-successful.loggable.ts @@ -0,0 +1,16 @@ +import { EntityId, UserLoginMigrationDO } from '@shared/domain'; +import { Loggable, LogMessage } from '@src/core/logger'; + +export class UserMigrationSuccessfulLoggable implements Loggable { + constructor(private readonly userId: EntityId, private readonly userLoginMigration: UserLoginMigrationDO) {} + + getLogMessage(): LogMessage { + return { + message: 'A user has successfully migrated.', + data: { + userId: this.userId, + userLoginMigrationId: this.userLoginMigration.id, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.spec.ts new file mode 100644 index 00000000000..0819925d7d8 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.spec.ts @@ -0,0 +1,30 @@ +import { ExternalSchoolNumberMissingLoggableException } from './external-school-number-missing.loggable-exception'; + +describe(ExternalSchoolNumberMissingLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const externalSchoolId = 'externalSchoolId'; + const exception = new ExternalSchoolNumberMissingLoggableException(externalSchoolId); + + return { + exception, + externalSchoolId, + }; + }; + + it('should return the correct log message', () => { + const { exception, externalSchoolId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'EXTERNAL_SCHOOL_NUMBER_MISSING', + message: 'The external system did not provide a official school number for the school.', + stack: expect.any(String), + data: { + externalSchoolId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.ts new file mode 100644 index 00000000000..6c94a05e2e5 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/external-school-number-missing.loggable-exception.ts @@ -0,0 +1,19 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class ExternalSchoolNumberMissingLoggableException extends UnprocessableEntityException implements Loggable { + constructor(private readonly externalSchoolId: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'EXTERNAL_SCHOOL_NUMBER_MISSING', + message: 'The external system did not provide a official school number for the school.', + stack: this.stack, + data: { + externalSchoolId: this.externalSchoolId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/index.ts b/apps/server/src/modules/user-login-migration/loggable/index.ts index 28cdc9f6baa..e31de36b72a 100644 --- a/apps/server/src/modules/user-login-migration/loggable/index.ts +++ b/apps/server/src/modules/user-login-migration/loggable/index.ts @@ -1,2 +1,12 @@ export * from './user-login-migration-start.loggable'; export * from './user-login-migration-mandatory.loggable'; +export * from './school-number-missing.loggable-exception'; +export * from './user-login-migration-already-closed.loggable-exception'; +export * from './user-login-migration-grace-period-expired-loggable.exception'; +export * from './user-login-migration-not-found.loggable-exception'; +export * from './school-number-mismatch.loggable-exception'; +export * from './external-school-number-missing.loggable-exception'; +export * from './user-migration-database-operation-failed.loggable-exception'; +export * from './school-migration-database-operation-failed.loggable-exception'; +export * from './invalid-user-login-migration.loggable-exception'; +export * from './debug'; diff --git a/apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.spec.ts new file mode 100644 index 00000000000..b0f50611c29 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.spec.ts @@ -0,0 +1,35 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { InvalidUserLoginMigrationLoggableException } from './invalid-user-login-migration.loggable-exception'; + +describe(InvalidUserLoginMigrationLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const targetSystemId = new ObjectId().toHexString(); + + const exception = new InvalidUserLoginMigrationLoggableException(userId, targetSystemId); + + return { + exception, + userId, + targetSystemId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, targetSystemId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'INVALID_USER_LOGIN_MIGRATION', + message: 'The migration cannot be started, because there is no migration to the selected target system.', + stack: expect.any(String), + data: { + userId, + targetSystemId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.ts new file mode 100644 index 00000000000..8e24483de1b --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/invalid-user-login-migration.loggable-exception.ts @@ -0,0 +1,21 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class InvalidUserLoginMigrationLoggableException extends UnprocessableEntityException implements Loggable { + constructor(private readonly userId: EntityId, private readonly targetSystemId: EntityId) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'INVALID_USER_LOGIN_MIGRATION', + message: 'The migration cannot be started, because there is no migration to the selected target system.', + stack: this.stack, + data: { + userId: this.userId, + targetSystemId: this.targetSystemId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.spec.ts new file mode 100644 index 00000000000..701b5a6a4fe --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.spec.ts @@ -0,0 +1,32 @@ +import { legacySchoolDoFactory } from '@shared/testing'; +import { SchoolMigrationDatabaseOperationFailedLoggableException } from './school-migration-database-operation-failed.loggable-exception'; + +describe(SchoolMigrationDatabaseOperationFailedLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const school = legacySchoolDoFactory.buildWithId(); + + const exception = new SchoolMigrationDatabaseOperationFailedLoggableException(school, 'migration', new Error()); + + return { + exception, + school, + }; + }; + + it('should return the correct log message', () => { + const { exception, school } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'SCHOOL_LOGIN_MIGRATION_DATABASE_OPERATION_FAILED', + stack: expect.any(String), + data: { + schoolId: school.id, + operation: 'migration', + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.ts new file mode 100644 index 00000000000..b31e2663701 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/school-migration-database-operation-failed.loggable-exception.ts @@ -0,0 +1,29 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { LegacySchoolDo } from '@shared/domain'; +import { ErrorUtils } from '@src/core/error/utils'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; + +export class SchoolMigrationDatabaseOperationFailedLoggableException + extends InternalServerErrorException + implements Loggable +{ + // TODO: Remove undefined type from schoolId when using the new School DO + constructor( + private readonly school: LegacySchoolDo, + private readonly operation: 'migration' | 'rollback', + error: unknown + ) { + super(ErrorUtils.createHttpExceptionOptions(error)); + } + + public getLogMessage(): ErrorLogMessage { + return { + type: 'SCHOOL_LOGIN_MIGRATION_DATABASE_OPERATION_FAILED', + stack: this.stack, + data: { + schoolId: this.school.id, + operation: this.operation, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.spec.ts new file mode 100644 index 00000000000..caa39202746 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.spec.ts @@ -0,0 +1,34 @@ +import { SchoolNumberMismatchLoggableException } from './school-number-mismatch.loggable-exception'; + +describe(SchoolNumberMismatchLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const sourceSchoolNumber = '123'; + const targetSchoolNumber = '456'; + + const exception = new SchoolNumberMismatchLoggableException(sourceSchoolNumber, targetSchoolNumber); + + return { + exception, + sourceSchoolNumber, + targetSchoolNumber, + }; + }; + + it('should return the correct log message', () => { + const { exception, sourceSchoolNumber, targetSchoolNumber } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'SCHOOL_MIGRATION_FAILED', + message: 'School could not migrate during user migration process.', + stack: expect.any(String), + data: { + sourceSchoolNumber, + targetSchoolNumber, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.ts new file mode 100644 index 00000000000..93bd64c2ecf --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/school-number-mismatch.loggable-exception.ts @@ -0,0 +1,32 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class SchoolNumberMismatchLoggableException extends BusinessError implements Loggable { + constructor(private readonly sourceSchoolNumber: string, private readonly targetSchoolNumber: string) { + super( + { + type: 'SCHOOL_MIGRATION_FAILED', + title: 'Migration of school failed.', + defaultMessage: 'School could not migrate during user migration process.', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + { + sourceSchoolNumber, + targetSchoolNumber, + } + ); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: this.type, + message: this.message, + stack: this.stack, + data: { + sourceSchoolNumber: this.sourceSchoolNumber, + targetSchoolNumber: this.targetSchoolNumber, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/school-number-missing.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/school-number-missing.loggable-exception.spec.ts new file mode 100644 index 00000000000..ff7a4d2fc39 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/school-number-missing.loggable-exception.spec.ts @@ -0,0 +1,32 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { SchoolNumberMissingLoggableException } from './school-number-missing.loggable-exception'; + +describe(SchoolNumberMissingLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + + const exception = new SchoolNumberMissingLoggableException(schoolId); + + return { + exception, + schoolId, + }; + }; + + it('should return the correct log message', () => { + const { exception, schoolId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'SCHOOL_NUMBER_MISSING', + message: 'The school is missing a official school number.', + stack: expect.any(String), + data: { + schoolId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/error/school-number-missing.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/school-number-missing.loggable-exception.ts similarity index 100% rename from apps/server/src/modules/user-login-migration/error/school-number-missing.loggable-exception.ts rename to apps/server/src/modules/user-login-migration/loggable/school-number-missing.loggable-exception.ts diff --git a/apps/server/src/modules/user-login-migration/error/user-login-migration-already-closed.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts similarity index 100% rename from apps/server/src/modules/user-login-migration/error/user-login-migration-already-closed.loggable-exception.ts rename to apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts diff --git a/apps/server/src/modules/user-login-migration/error/user-login-migration-grace-period-expired-loggable.exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-grace-period-expired-loggable.exception.ts similarity index 100% rename from apps/server/src/modules/user-login-migration/error/user-login-migration-grace-period-expired-loggable.exception.ts rename to apps/server/src/modules/user-login-migration/loggable/user-login-migration-grace-period-expired-loggable.exception.ts diff --git a/apps/server/src/modules/user-login-migration/error/user-login-migration-not-found.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-not-found.loggable-exception.ts similarity index 100% rename from apps/server/src/modules/user-login-migration/error/user-login-migration-not-found.loggable-exception.ts rename to apps/server/src/modules/user-login-migration/loggable/user-login-migration-not-found.loggable-exception.ts diff --git a/apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.spec.ts new file mode 100644 index 00000000000..a83d1264ff0 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.spec.ts @@ -0,0 +1,32 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { UserMigrationDatabaseOperationFailedLoggableException } from './user-migration-database-operation-failed.loggable-exception'; + +describe(UserMigrationDatabaseOperationFailedLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + + const exception = new UserMigrationDatabaseOperationFailedLoggableException(userId, 'migration', new Error()); + + return { + exception, + userId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'USER_LOGIN_MIGRATION_DATABASE_OPERATION_FAILED', + stack: expect.any(String), + data: { + userId, + operation: 'migration', + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.ts new file mode 100644 index 00000000000..39e41b9830a --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-migration-database-operation-failed.loggable-exception.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ErrorUtils } from '@src/core/error/utils'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; + +export class UserMigrationDatabaseOperationFailedLoggableException + extends InternalServerErrorException + implements Loggable +{ + constructor(private readonly userId: EntityId, private readonly operation: 'migration' | 'rollback', error: unknown) { + super(ErrorUtils.createHttpExceptionOptions(error)); + } + + public getLogMessage(): ErrorLogMessage { + return { + type: 'USER_LOGIN_MIGRATION_DATABASE_OPERATION_FAILED', + stack: this.stack, + data: { + userId: this.userId, + operation: this.operation, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/mapper/index.ts b/apps/server/src/modules/user-login-migration/mapper/index.ts index 03b0a12e9be..7cf4b79d56c 100644 --- a/apps/server/src/modules/user-login-migration/mapper/index.ts +++ b/apps/server/src/modules/user-login-migration/mapper/index.ts @@ -1,2 +1 @@ -export * from './page-content.mapper'; export * from './user-login-migration.mapper'; diff --git a/apps/server/src/modules/user-login-migration/mapper/page-content.mapper.spec.ts b/apps/server/src/modules/user-login-migration/mapper/page-content.mapper.spec.ts deleted file mode 100644 index e3b1e538c47..00000000000 --- a/apps/server/src/modules/user-login-migration/mapper/page-content.mapper.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { PageContentMapper } from './page-content.mapper'; -import { PageContentDto } from '../service/dto/page-content.dto'; - -describe('PageContentMapper', () => { - let module: TestingModule; - let mapper: PageContentMapper; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [PageContentMapper], - }).compile(); - mapper = module.get(PageContentMapper); - }); - - afterAll(async () => { - await module.close(); - }); - - const setup = () => { - const dto: PageContentDto = { - proceedButtonUrl: 'proceed', - cancelButtonUrl: 'cancel', - }; - return { dto }; - }; - - describe('mapDtoToResponse is called', () => { - describe('when it maps from dto to response', () => { - it('should map the dto to a response', () => { - const { dto } = setup(); - const response = mapper.mapDtoToResponse(dto); - expect(response.proceedButtonUrl).toEqual(dto.proceedButtonUrl); - expect(response.cancelButtonUrl).toEqual(dto.cancelButtonUrl); - }); - }); - }); -}); diff --git a/apps/server/src/modules/user-login-migration/mapper/page-content.mapper.ts b/apps/server/src/modules/user-login-migration/mapper/page-content.mapper.ts deleted file mode 100644 index 7abec9c2208..00000000000 --- a/apps/server/src/modules/user-login-migration/mapper/page-content.mapper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PageContentDto } from '../service/dto/page-content.dto'; -import { PageContentResponse } from '../controller/dto'; - -@Injectable() -export class PageContentMapper { - mapDtoToResponse(dto: PageContentDto): PageContentResponse { - const response: PageContentResponse = new PageContentResponse({ - proceedButtonUrl: dto.proceedButtonUrl, - cancelButtonUrl: dto.cancelButtonUrl, - }); - - return response; - } -} diff --git a/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts b/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts index 272e2309392..1fecfa7f7aa 100644 --- a/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts +++ b/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts @@ -7,6 +7,7 @@ export class UserLoginMigrationMapper { const query: UserLoginMigrationQuery = { userId: searchParams.userId, }; + return query; } @@ -20,6 +21,7 @@ export class UserLoginMigrationMapper { finishedAt: domainObject.finishedAt, mandatorySince: domainObject.mandatorySince, }); + return response; } } diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts index 8addd2afae6..988e6de9b01 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts @@ -1,25 +1,26 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { UnprocessableEntityException } from '@nestjs/common'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; -import { ValidationError } from '@shared/common'; import { LegacySchoolDo, Page, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo/userloginmigration/user-login-migration.repo'; import { legacySchoolDoFactory, setupEntities, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@modules/authentication'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; -import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error'; +import { LegacyLogger, Logger } from '@src/core/logger'; +import { + SchoolMigrationDatabaseOperationFailedLoggableException, + SchoolNumberMismatchLoggableException, +} from '../loggable'; import { SchoolMigrationService } from './school-migration.service'; -describe('SchoolMigrationService', () => { +describe(SchoolMigrationService.name, () => { let module: TestingModule; let service: SchoolMigrationService; let userService: DeepMocked; let schoolService: DeepMocked; let userLoginMigrationRepo: DeepMocked; + let logger: DeepMocked; beforeAll(async () => { jest.useFakeTimers(); @@ -39,6 +40,10 @@ describe('SchoolMigrationService', () => { provide: LegacyLogger, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, { provide: UserLoginMigrationRepo, useValue: createMock(), @@ -50,6 +55,7 @@ describe('SchoolMigrationService', () => { schoolService = module.get(LegacySchoolService); userService = module.get(UserService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); + logger = module.get(Logger); await setupEntities(); }); @@ -59,319 +65,234 @@ describe('SchoolMigrationService', () => { await module.close(); }); - describe('validateGracePeriod is called', () => { - describe('when current date is before finish date', () => { + describe('migrateSchool', () => { + describe('when a school without systems successfully migrates', () => { const setup = () => { - jest.setSystemTime(new Date('2023-05-01')); - - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - schoolId: 'schoolId', - targetSystemId: 'systemId', - startedAt: new Date('2023-05-01'), - closedAt: new Date('2023-05-01'), - finishedAt: new Date('2023-05-02'), + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + id: 'schoolId', + name: 'schoolName', + officialSchoolNumber: 'officialSchoolNumber', + externalId: 'firstExternalId', + systems: undefined, }); + const targetSystemId = 'targetSystemId'; + const targetExternalId = 'targetExternalId'; return { - userLoginMigration, + school, + targetSystemId, + sourceExternalId: school.externalId, + targetExternalId, }; }; - it('should not throw', () => { - const { userLoginMigration } = setup(); - - const func = () => service.validateGracePeriod(userLoginMigration); - - expect(func).not.toThrow(); - }); - }); + it('should save the migrated school and add the system', async () => { + const { school, targetSystemId, targetExternalId, sourceExternalId } = setup(); - describe('when current date is after finish date', () => { - const setup = () => { - jest.setSystemTime(new Date('2023-05-03')); + await service.migrateSchool({ ...school }, targetExternalId, targetSystemId); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - schoolId: 'schoolId', - targetSystemId: 'systemId', - startedAt: new Date('2023-05-01'), - closedAt: new Date('2023-05-01'), - finishedAt: new Date('2023-05-02'), + expect(schoolService.save).toHaveBeenCalledWith({ + ...school, + externalId: targetExternalId, + previousExternalId: sourceExternalId, + systems: [targetSystemId], }); - - return { - userLoginMigration, - }; - }; - - it('should throw validation error', () => { - const { userLoginMigration } = setup(); - - const func = () => service.validateGracePeriod(userLoginMigration); - - expect(func).toThrow( - new ValidationError('grace_period_expired: The grace period after finishing migration has expired') - ); }); }); - }); - describe('schoolToMigrate is called', () => { - describe('when school number is missing', () => { + describe('when a school with systems successfully migrates', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', externalId: 'firstExternalId', + systems: ['otherSystemId'], }); - - const userDO: UserDO = userDoFactory.buildWithId({ schoolId: schoolDO.id }, new ObjectId().toHexString(), {}); - - const currentUser: ICurrentUser = { - userId: userDO.id, - schoolId: userDO.schoolId, - systemId: 'systemId', - } as ICurrentUser; + const targetSystemId = 'targetSystemId'; + const targetExternalId = 'targetExternalId'; return { - externalId: schoolDO.externalId as string, - currentUser, + school, + targetSystemId, + sourceExternalId: school.externalId, + targetExternalId, }; }; - it('should throw an error', async () => { - const { currentUser, externalId } = setup(); + it('should save the migrated school and add the system', async () => { + const { school, targetSystemId, targetExternalId, sourceExternalId } = setup(); - const func = () => service.schoolToMigrate(currentUser.userId, externalId, undefined); + await service.migrateSchool({ ...school }, targetExternalId, targetSystemId); - await expect(func()).rejects.toThrow( - new OAuthMigrationError( - 'Official school number from target migration system is missing', - 'ext_official_school_number_missing' - ) - ); + expect(schoolService.save).toHaveBeenCalledWith({ + ...school, + externalId: targetExternalId, + previousExternalId: sourceExternalId, + systems: ['otherSystemId', targetSystemId], + }); }); }); - describe('when school could not be found with official school number', () => { + describe('when saving to the database fails', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', externalId: 'firstExternalId', }); + const targetSystemId = 'targetSystemId'; + const targetExternalId = 'targetExternalId'; + + const error = new Error('Cannot save'); - const userDO: UserDO = userDoFactory.buildWithId({ schoolId: schoolDO.id }, new ObjectId().toHexString(), {}); + schoolService.save.mockRejectedValueOnce(error); + schoolService.save.mockRejectedValueOnce(error); return { - currentUserId: userDO.id as string, - officialSchoolNumber: schoolDO.officialSchoolNumber, - schoolDO, - externalId: schoolDO.externalId as string, - userDO, + school, + targetSystemId, + sourceExternalId: school.externalId, + targetExternalId, + error, }; }; - it('should throw an error', async () => { - const { currentUserId, externalId, officialSchoolNumber, userDO, schoolDO } = setup(); - userService.findById.mockResolvedValue(userDO); - schoolService.getSchoolById.mockResolvedValue(schoolDO); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(null); - - const func = () => service.schoolToMigrate(currentUserId, externalId, officialSchoolNumber); - - await expect(func()).rejects.toThrow( - new OAuthMigrationError( - 'Could not find school by official school number from target migration system', - 'ext_official_school_missing' - ) - ); + it('should roll back any changes to the school', async () => { + const { school, targetSystemId, targetExternalId } = setup(); + + await expect(service.migrateSchool({ ...school }, targetExternalId, targetSystemId)).rejects.toThrow(); + + expect(schoolService.save).toHaveBeenLastCalledWith(school); }); - }); - describe('when current users school not match with school of to migrate user ', () => { - const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ - id: 'schoolId', - name: 'schoolName', - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'firstExternalId', - }); + it('should log a rollback error', async () => { + const { school, targetSystemId, targetExternalId, error } = setup(); - const userDO: UserDO = userDoFactory.buildWithId({ schoolId: schoolDO.id }, new ObjectId().toHexString(), {}); + await expect(service.migrateSchool({ ...school }, targetExternalId, targetSystemId)).rejects.toThrow(); - return { - currentUserId: userDO.id as string, - schoolDO, - externalId: schoolDO.externalId as string, - userDO, - }; - }; + expect(logger.warning).toHaveBeenCalledWith( + new SchoolMigrationDatabaseOperationFailedLoggableException(school, 'rollback', error) + ); + }); it('should throw an error', async () => { - const { currentUserId, externalId, schoolDO, userDO } = setup(); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(schoolDO); - schoolDO.officialSchoolNumber = 'OfficialSchoolnumberMismatch'; - schoolService.getSchoolById.mockResolvedValue(schoolDO); - - userService.findById.mockResolvedValue(userDO); - - const func = () => service.schoolToMigrate(currentUserId, externalId, 'targetSchoolNumber'); - - await expect(func()).rejects.toThrow( - new OAuthMigrationError( - 'Current users school is not the same as school found by official school number from target migration system', - 'ext_official_school_number_mismatch', - 'targetSchoolNumber', - schoolDO.officialSchoolNumber - ) + const { school, targetSystemId, targetExternalId, error } = setup(); + + await expect(service.migrateSchool({ ...school }, targetExternalId, targetSystemId)).rejects.toThrow( + new SchoolMigrationDatabaseOperationFailedLoggableException(school, 'migration', error) ); }); }); + }); - describe('when school was already migrated', () => { + describe('getSchoolForMigration', () => { + describe('when the school has to be migrated', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const officialSchoolNumber = 'officialSchoolNumber'; + const sourceExternalId = 'sourceExternalId'; + const targetExternalId = 'targetExternalId'; + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ id: 'schoolId', name: 'schoolName', - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'firstExternalId', + officialSchoolNumber, + externalId: sourceExternalId, }); - const userDO: UserDO = userDoFactory.buildWithId({ schoolId: schoolDO.id }, new ObjectId().toHexString(), {}); + const user: UserDO = userDoFactory.build({ id: new ObjectId().toHexString(), schoolId: school.id }); + + userService.findById.mockResolvedValue(user); + schoolService.getSchoolById.mockResolvedValue(school); return { - currentUserId: userDO.id as string, - schoolDO, - externalId: schoolDO.externalId as string, - userDO, + userId: user.id as string, + user, + officialSchoolNumber, + school, + targetExternalId, }; }; - it('should return null ', async () => { - const { currentUserId, externalId, schoolDO, userDO } = setup(); - userService.findById.mockResolvedValue(userDO); - schoolService.getSchoolById.mockResolvedValue(schoolDO); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(schoolDO); + it('should return the school', async () => { + const { userId, targetExternalId, officialSchoolNumber, school } = setup(); - const result: LegacySchoolDo | null = await service.schoolToMigrate( - currentUserId, - externalId, - schoolDO.officialSchoolNumber - ); + const result = await service.getSchoolForMigration(userId, targetExternalId, officialSchoolNumber); - expect(result).toBeNull(); + expect(result).toEqual(school); }); }); - describe('when school has to be migrated', () => { + describe('when the school is already migrated', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const officialSchoolNumber = 'officialSchoolNumber'; + const sourceExternalId = 'sourceExternalId'; + const targetExternalId = 'targetExternalId'; + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ id: 'schoolId', name: 'schoolName', - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'firstExternalId', + officialSchoolNumber, + externalId: targetExternalId, + previousExternalId: sourceExternalId, }); - const userDO: UserDO = userDoFactory.buildWithId({ schoolId: schoolDO.id }, new ObjectId().toHexString(), {}); + const user: UserDO = userDoFactory.build({ id: new ObjectId().toHexString(), schoolId: school.id }); + + userService.findById.mockResolvedValue(user); + schoolService.getSchoolById.mockResolvedValue(school); return { - currentUserId: userDO.id as string, - schoolDO, - userDO, + userId: user.id as string, + user, + officialSchoolNumber, + school, + targetExternalId, }; }; - it('should return migrated school', async () => { - const { currentUserId, schoolDO, userDO } = setup(); - schoolService.getSchoolById.mockResolvedValue(schoolDO); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(schoolDO); - userService.findById.mockResolvedValue(userDO); + it('should return null', async () => { + const { userId, targetExternalId, officialSchoolNumber } = setup(); - const result: LegacySchoolDo | null = await service.schoolToMigrate( - currentUserId, - 'newExternalId', - schoolDO.officialSchoolNumber - ); + const result = await service.getSchoolForMigration(userId, targetExternalId, officialSchoolNumber); - expect(result).toEqual(schoolDO); + expect(result).toBeNull(); }); }); - }); - describe('migrateSchool is called', () => { - describe('when school will be migrated', () => { + describe('when the school number from the external system is not the same as the school number of the users school', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const officialSchoolNumber = 'officialSchoolNumber'; + const otherOfficialSchoolNumber = 'notTheSameOfficialSchoolNumber'; + const sourceExternalId = 'sourceExternalId'; + const targetExternalId = 'targetExternalId'; + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ id: 'schoolId', name: 'schoolName', - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'firstExternalId', + officialSchoolNumber, + externalId: sourceExternalId, }); - const targetSystemId = 'targetSystemId'; + + const user: UserDO = userDoFactory.build({ id: new ObjectId().toHexString(), schoolId: school.id }); + + userService.findById.mockResolvedValue(user); + schoolService.getSchoolById.mockResolvedValue(school); return { - schoolDO, - targetSystemId, - firstExternalId: schoolDO.externalId, + userId: user.id as string, + user, + officialSchoolNumber, + otherOfficialSchoolNumber, + school, + targetExternalId, }; }; - it('should save the migrated school', async () => { - const { schoolDO, targetSystemId, firstExternalId } = setup(); - const newExternalId = 'newExternalId'; - - await service.migrateSchool(newExternalId, schoolDO, targetSystemId); - - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - systems: [targetSystemId], - previousExternalId: firstExternalId, - externalId: newExternalId, - }) - ); - }); - - describe('when there are other systems before', () => { - it('should add the system to migrated school', async () => { - const { schoolDO, targetSystemId } = setup(); - schoolDO.systems = ['existingSystem']; - - await service.migrateSchool('newExternalId', schoolDO, targetSystemId); - - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - systems: ['existingSystem', targetSystemId], - }) - ); - }); - }); - - describe('when there are no systems in School', () => { - it('should add the system to migrated school', async () => { - const { schoolDO, targetSystemId } = setup(); - schoolDO.systems = undefined; - - await service.migrateSchool('newExternalId', schoolDO, targetSystemId); - - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - systems: [targetSystemId], - }) - ); - }); - }); - - describe('when an error occurred', () => { - it('should save the old schoolDo (rollback the migration)', async () => { - const { schoolDO, targetSystemId } = setup(); - schoolService.save.mockRejectedValueOnce(new Error()); + it('should throw a school number mismatch error', async () => { + const { userId, targetExternalId, officialSchoolNumber, otherOfficialSchoolNumber } = setup(); - await service.migrateSchool('newExternalId', schoolDO, targetSystemId); - - expect(schoolService.save).toHaveBeenCalledWith(schoolDO); - }); + await expect( + service.getSchoolForMigration(userId, targetExternalId, otherOfficialSchoolNumber) + ).rejects.toThrow(new SchoolNumberMismatchLoggableException(officialSchoolNumber, otherOfficialSchoolNumber)); }); }); }); @@ -391,18 +312,18 @@ describe('SchoolMigrationService', () => { const users: UserDO[] = userDoFactory.buildListWithId(3, { outdatedSince: undefined }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); userService.findUsers.mockResolvedValue(new Page(users, users.length)); return { closedAt, + userLoginMigration, }; }; it('should save migrated user with removed outdatedSince entry', async () => { - const { closedAt } = setup(); + const { closedAt, userLoginMigration } = setup(); - await service.markUnmigratedUsersAsOutdated('schoolId'); + await service.markUnmigratedUsersAsOutdated(userLoginMigration); expect(userService.saveAll).toHaveBeenCalledWith([ expect.objectContaining>({ outdatedSince: closedAt }), @@ -411,20 +332,6 @@ describe('SchoolMigrationService', () => { ]); }); }); - - describe('when the school has no migration', () => { - const setup = () => { - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - }; - - it('should throw an UnprocessableEntityException', async () => { - setup(); - - const func = async () => service.markUnmigratedUsersAsOutdated('schoolId'); - - await expect(func).rejects.toThrow(UnprocessableEntityException); - }); - }); }); describe('unmarkOutdatedUsers', () => { @@ -440,14 +347,17 @@ describe('SchoolMigrationService', () => { const users: UserDO[] = userDoFactory.buildListWithId(3, { outdatedSince: new Date('2023-05-02') }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); userService.findUsers.mockResolvedValue(new Page(users, users.length)); + + return { + userLoginMigration, + }; }; it('should save migrated user with removed outdatedSince entry', async () => { - setup(); + const { userLoginMigration } = setup(); - await service.unmarkOutdatedUsers('schoolId'); + await service.unmarkOutdatedUsers(userLoginMigration); expect(userService.saveAll).toHaveBeenCalledWith([ expect.objectContaining>({ outdatedSince: undefined }), @@ -456,20 +366,6 @@ describe('SchoolMigrationService', () => { ]); }); }); - - describe('when the school has no migration', () => { - const setup = () => { - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - }; - - it('should throw an UnprocessableEntityException', async () => { - setup(); - - const func = async () => service.unmarkOutdatedUsers('schoolId'); - - await expect(func).rejects.toThrow(UnprocessableEntityException); - }); - }); }); describe('hasSchoolMigratedUser', () => { diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts index 147d9ec112b..aa5173dcff6 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts @@ -1,94 +1,99 @@ -import { Injectable, UnprocessableEntityException } from '@nestjs/common'; -import { ValidationError } from '@shared/common'; -import { Page, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; -import { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacyLogger } from '@src/core/logger'; import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; +import { Injectable } from '@nestjs/common'; +import { LegacySchoolDo, Page, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { UserLoginMigrationRepo } from '@shared/repo'; +import { LegacyLogger, Logger } from '@src/core/logger'; import { performance } from 'perf_hooks'; -import { OAuthMigrationError } from '../error'; +import { + SchoolMigrationDatabaseOperationFailedLoggableException, + SchoolNumberMismatchLoggableException, +} from '../loggable'; @Injectable() export class SchoolMigrationService { constructor( private readonly schoolService: LegacySchoolService, - private readonly logger: LegacyLogger, + private readonly legacyLogger: LegacyLogger, + private readonly logger: Logger, private readonly userService: UserService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo ) {} - validateGracePeriod(userLoginMigration: UserLoginMigrationDO) { - if (userLoginMigration.finishedAt && Date.now() >= userLoginMigration.finishedAt.getTime()) { - throw new ValidationError('grace_period_expired: The grace period after finishing migration has expired', { - finishedAt: userLoginMigration.finishedAt, - }); - } - } - - async migrateSchool(externalId: string, existingSchool: LegacySchoolDo, targetSystemId: string): Promise { + async migrateSchool(existingSchool: LegacySchoolDo, externalId: string, targetSystemId: string): Promise { const schoolDOCopy: LegacySchoolDo = new LegacySchoolDo({ ...existingSchool }); try { await this.doMigration(externalId, existingSchool, targetSystemId); - } catch (e: unknown) { - await this.rollbackMigration(schoolDOCopy); - this.logger.log({ - message: `This error occurred during migration of School with official school number`, - officialSchoolNumber: existingSchool.officialSchoolNumber, - error: e, - }); + } catch (error: unknown) { + await this.tryRollbackMigration(schoolDOCopy); + + throw new SchoolMigrationDatabaseOperationFailedLoggableException(existingSchool, 'migration', error); } } - async schoolToMigrate( - currentUserId: string, - externalId: string, - officialSchoolNumber: string | undefined - ): Promise { - if (!officialSchoolNumber) { - throw new OAuthMigrationError( - 'Official school number from target migration system is missing', - 'ext_official_school_number_missing' - ); + private async doMigration(externalId: string, school: LegacySchoolDo, targetSystemId: string): Promise { + school.previousExternalId = school.externalId; + school.externalId = externalId; + if (!school.systems) { + school.systems = []; } - - const userDO: UserDO | null = await this.userService.findById(currentUserId); - if (userDO) { - const schoolDO: LegacySchoolDo = await this.schoolService.getSchoolById(userDO.schoolId); - this.checkOfficialSchoolNumbersMatch(schoolDO, officialSchoolNumber); + if (!school.systems.includes(targetSystemId)) { + school.systems.push(targetSystemId); } - const existingSchool: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber( - officialSchoolNumber - ); + await this.schoolService.save(school); + } - if (!existingSchool) { - throw new OAuthMigrationError( - 'Could not find school by official school number from target migration system', - 'ext_official_school_missing' + private async tryRollbackMigration(originalSchoolDO: LegacySchoolDo) { + try { + await this.schoolService.save(originalSchoolDO); + } catch (error: unknown) { + this.logger.warning( + new SchoolMigrationDatabaseOperationFailedLoggableException(originalSchoolDO, 'rollback', error) ); } + } - const schoolMigrated: boolean = this.hasSchoolMigrated(externalId, existingSchool.externalId); + async getSchoolForMigration( + userId: string, + externalId: string, + officialSchoolNumber: string + ): Promise { + const user: UserDO = await this.userService.findById(userId); + const school: LegacySchoolDo = await this.schoolService.getSchoolById(user.schoolId); + + this.checkOfficialSchoolNumbersMatch(school, officialSchoolNumber); + + const schoolMigrated: boolean = this.hasSchoolMigrated(school.externalId, externalId); if (schoolMigrated) { return null; } - return existingSchool; + return school; } - async markUnmigratedUsersAsOutdated(schoolId: string): Promise { - const startTime: number = performance.now(); + private checkOfficialSchoolNumbersMatch(schoolDO: LegacySchoolDo, officialExternalSchoolNumber: string): void { + if (schoolDO.officialSchoolNumber !== officialExternalSchoolNumber) { + throw new SchoolNumberMismatchLoggableException( + schoolDO.officialSchoolNumber ?? '', + officialExternalSchoolNumber + ); + } + } - const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); + private hasSchoolMigrated(sourceExternalId: string | undefined, targetExternalId: string): boolean { + const isExternalIdEquivalent: boolean = sourceExternalId === targetExternalId; - if (!userLoginMigration) { - throw new UnprocessableEntityException(`School ${schoolId} has no UserLoginMigration`); - } + return isExternalIdEquivalent; + } + + async markUnmigratedUsersAsOutdated(userLoginMigration: UserLoginMigrationDO): Promise { + const startTime: number = performance.now(); const notMigratedUsers: Page = await this.userService.findUsers({ - schoolId, + schoolId: userLoginMigration.schoolId, isOutdated: false, lastLoginSystemChangeSmallerThan: userLoginMigration.startedAt, }); @@ -100,20 +105,18 @@ export class SchoolMigrationService { await this.userService.saveAll(notMigratedUsers.data); const endTime: number = performance.now(); - this.logger.warn(`completeMigration for schoolId ${schoolId} took ${endTime - startTime} milliseconds`); + this.legacyLogger.warn( + `markUnmigratedUsersAsOutdated for schoolId ${userLoginMigration.schoolId} took ${ + endTime - startTime + } milliseconds` + ); } - async unmarkOutdatedUsers(schoolId: string): Promise { + async unmarkOutdatedUsers(userLoginMigration: UserLoginMigrationDO): Promise { const startTime: number = performance.now(); - const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); - - if (!userLoginMigration) { - throw new UnprocessableEntityException(`School ${schoolId} has no UserLoginMigration`); - } - const migratedUsers: Page = await this.userService.findUsers({ - schoolId, + schoolId: userLoginMigration.schoolId, outdatedSince: userLoginMigration.finishedAt, }); @@ -124,42 +127,9 @@ export class SchoolMigrationService { await this.userService.saveAll(migratedUsers.data); const endTime: number = performance.now(); - this.logger.warn(`restartMigration for schoolId ${schoolId} took ${endTime - startTime} milliseconds`); - } - - private async doMigration(externalId: string, schoolDO: LegacySchoolDo, targetSystemId: string): Promise { - if (schoolDO.systems) { - schoolDO.systems.push(targetSystemId); - } else { - schoolDO.systems = [targetSystemId]; - } - schoolDO.previousExternalId = schoolDO.externalId; - schoolDO.externalId = externalId; - await this.schoolService.save(schoolDO); - } - - private async rollbackMigration(originalSchoolDO: LegacySchoolDo) { - if (originalSchoolDO) { - await this.schoolService.save(originalSchoolDO); - } - } - - private checkOfficialSchoolNumbersMatch(schoolDO: LegacySchoolDo, officialExternalSchoolNumber: string): void { - if (schoolDO.officialSchoolNumber !== officialExternalSchoolNumber) { - throw new OAuthMigrationError( - 'Current users school is not the same as school found by official school number from target migration system', - 'ext_official_school_number_mismatch', - schoolDO.officialSchoolNumber, - officialExternalSchoolNumber - ); - } - } - - private hasSchoolMigrated(sourceExternalId: string, targetExternalId?: string): boolean { - if (sourceExternalId === targetExternalId) { - return true; - } - return false; + this.legacyLogger.warn( + `unmarkOutdatedUsers for schoolId ${userLoginMigration.schoolId} took ${endTime - startTime} milliseconds` + ); } async hasSchoolMigratedUser(schoolId: string): Promise { diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts index 01e12e0df19..ac7f3bc4740 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts @@ -1,20 +1,22 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ObjectId } from '@mikro-orm/mongodb'; -import { InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { EntityId, LegacySchoolDo, SchoolFeatures, UserDO, UserLoginMigrationDO } from '@shared/domain'; -import { UserLoginMigrationRepo } from '@shared/repo'; -import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { LegacySchoolService } from '@modules/legacy-school'; import { SystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; -import { UserLoginMigrationNotFoundLoggableException } from '../error'; -import { SchoolMigrationService } from './school-migration.service'; +import { InternalServerErrorException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { EntityId, LegacySchoolDo, SchoolFeatures, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { UserLoginMigrationRepo } from '@shared/repo'; +import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { + UserLoginMigrationGracePeriodExpiredLoggableException, + UserLoginMigrationNotFoundLoggableException, +} from '../loggable'; import { UserLoginMigrationService } from './user-login-migration.service'; -describe('UserLoginMigrationService', () => { +describe(UserLoginMigrationService.name, () => { let module: TestingModule; let service: UserLoginMigrationService; @@ -22,7 +24,6 @@ describe('UserLoginMigrationService', () => { let schoolService: DeepMocked; let systemService: DeepMocked; let userLoginMigrationRepo: DeepMocked; - let schoolMigrationService: DeepMocked; const mockedDate: Date = new Date('2023-05-02'); const finishDate: Date = new Date( @@ -52,10 +53,6 @@ describe('UserLoginMigrationService', () => { provide: UserLoginMigrationRepo, useValue: createMock(), }, - { - provide: SchoolMigrationService, - useValue: createMock(), - }, ], }).compile(); @@ -64,7 +61,6 @@ describe('UserLoginMigrationService', () => { schoolService = module.get(LegacySchoolService); systemService = module.get(SystemService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); - schoolMigrationService = module.get(SchoolMigrationService); }); afterAll(async () => { @@ -160,404 +156,6 @@ describe('UserLoginMigrationService', () => { }); }); - describe('setMigration', () => { - describe('when first starting the migration', () => { - describe('when the school has no systems', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - school, - targetSystemId, - }; - }; - - it('should save the UserLoginMigration with start date and target system', async () => { - const { schoolId, targetSystemId } = setup(); - const expected: UserLoginMigrationDO = new UserLoginMigrationDO({ - id: new ObjectId().toHexString(), - targetSystemId, - schoolId, - startedAt: mockedDate, - }); - userLoginMigrationRepo.save.mockResolvedValue(expected); - - const result: UserLoginMigrationDO = await service.setMigration(schoolId, true); - - expect(result).toEqual(expected); - }); - }); - - describe('when the school has systems', () => { - const setup = () => { - const sourceSystemId: EntityId = new ObjectId().toHexString(); - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [sourceSystemId] }, schoolId); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - school, - targetSystemId, - sourceSystemId, - }; - }; - - it('should save the UserLoginMigration with start date, target system and source system', async () => { - const { schoolId, targetSystemId, sourceSystemId } = setup(); - const expected: UserLoginMigrationDO = new UserLoginMigrationDO({ - id: new ObjectId().toHexString(), - sourceSystemId, - targetSystemId, - schoolId, - startedAt: mockedDate, - }); - userLoginMigrationRepo.save.mockResolvedValue(expected); - - const result: UserLoginMigrationDO = await service.setMigration(schoolId, true); - - expect(result).toEqual(expected); - }); - }); - - describe('when the school has a feature', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - school, - targetSystemId, - }; - }; - - it('should add the OAUTH_PROVISIONING_ENABLED feature to the schools feature list', async () => { - const { schoolId, school } = setup(); - const existingFeature: SchoolFeatures = 'otherFeature' as SchoolFeatures; - school.features = [existingFeature]; - - await service.setMigration(schoolId, true, undefined, undefined); - - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - features: [existingFeature, SchoolFeatures.OAUTH_PROVISIONING_ENABLED], - }) - ); - }); - }); - - describe('when the school has no features yet', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - school, - targetSystemId, - }; - }; - - it('should set the OAUTH_PROVISIONING_ENABLED feature for the school', async () => { - const { schoolId } = setup(); - - await service.setMigration(schoolId, true, undefined, undefined); - - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], - }) - ); - }); - }); - - describe('when modifying a migration that does not exist on the school', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - schoolService.getSchoolById.mockResolvedValue(school); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - school, - }; - }; - - it('should throw an UnprocessableEntityException', async () => { - const { schoolId } = setup(); - - const func = async () => service.setMigration(schoolId, undefined, true, true); - - await expect(func).rejects.toThrow(UnprocessableEntityException); - }); - }); - - describe('when creating a new migration but the SANIS system does not exist', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - school, - }; - }; - - it('should throw an InternalServerErrorException', async () => { - const { schoolId } = setup(); - - const func = async () => service.setMigration(schoolId, true); - - await expect(func).rejects.toThrow(InternalServerErrorException); - }); - }); - }); - - describe('when restarting the migration', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - const userLoginMigrationId: EntityId = new ObjectId().toHexString(); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - id: userLoginMigrationId, - targetSystemId, - schoolId, - startedAt: mockedDate, - closedAt: mockedDate, - finishedAt: finishDate, - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); - - return { - schoolId, - userLoginMigration, - }; - }; - - it('should save the UserLoginMigration without close date and finish date', async () => { - const { schoolId, userLoginMigration } = setup(); - const expected: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - ...userLoginMigration, - closedAt: undefined, - finishedAt: undefined, - }); - userLoginMigrationRepo.save.mockResolvedValue(expected); - - const result: UserLoginMigrationDO = await service.setMigration(schoolId, true, undefined, false); - - expect(result).toEqual(expected); - }); - }); - - describe('when setting the migration to mandatory', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - const userLoginMigrationId: EntityId = new ObjectId().toHexString(); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - id: userLoginMigrationId, - targetSystemId, - schoolId, - startedAt: mockedDate, - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); - - return { - schoolId, - userLoginMigration, - }; - }; - - it('should save the UserLoginMigration with mandatory date', async () => { - const { schoolId, userLoginMigration } = setup(); - const expected: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - ...userLoginMigration, - mandatorySince: mockedDate, - }); - userLoginMigrationRepo.save.mockResolvedValue(expected); - - const result: UserLoginMigrationDO = await service.setMigration(schoolId, undefined, true); - - expect(result).toEqual(expected); - }); - }); - - describe('when setting the migration back to optional', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - const userLoginMigrationId: EntityId = new ObjectId().toHexString(); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - id: userLoginMigrationId, - targetSystemId, - schoolId, - startedAt: mockedDate, - mandatorySince: mockedDate, - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); - - return { - schoolId, - userLoginMigration, - }; - }; - - it('should save the UserLoginMigration without mandatory date', async () => { - const { schoolId, userLoginMigration } = setup(); - const expected: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - ...userLoginMigration, - mandatorySince: undefined, - }); - userLoginMigrationRepo.save.mockResolvedValue(expected); - - const result: UserLoginMigrationDO = await service.setMigration(schoolId, undefined, false); - - expect(result).toEqual(expected); - }); - }); - - describe('when closing the migration', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ - id: targetSystemId, - type: 'oauth2', - alias: 'SANIS', - }); - - const userLoginMigrationId: EntityId = new ObjectId().toHexString(); - const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - id: userLoginMigrationId, - targetSystemId, - schoolId, - startedAt: mockedDate, - }); - - schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); - - return { - schoolId, - userLoginMigration, - }; - }; - - it('should call schoolService.removeFeature', async () => { - const { schoolId } = setup(); - - await service.setMigration(schoolId, undefined, undefined, true); - - expect(schoolService.removeFeature).toHaveBeenCalledWith( - schoolId, - SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION - ); - }); - - it('should save the UserLoginMigration with close date and finish date', async () => { - const { schoolId, userLoginMigration } = setup(); - const expected: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - ...userLoginMigration, - closedAt: mockedDate, - finishedAt: finishDate, - }); - userLoginMigrationRepo.save.mockResolvedValue(expected); - - const result: UserLoginMigrationDO = await service.setMigration(schoolId, undefined, undefined, true); - - expect(result).toEqual(expected); - }); - }); - }); - describe('startMigration', () => { describe('when schoolId is given', () => { const setup = () => { @@ -816,7 +414,7 @@ describe('UserLoginMigrationService', () => { it('should call userLoginMigrationRepo.delete', async () => { const { userLoginMigration } = setup(); - await service.deleteUserLoginMigration(userLoginMigration); + await service.deleteUserLoginMigration({ ...userLoginMigration }); expect(userLoginMigrationRepo.delete).toHaveBeenCalledWith(userLoginMigration); }); @@ -824,73 +422,100 @@ describe('UserLoginMigrationService', () => { }); describe('restartMigration', () => { - describe('when migration restart was successfully', () => { + describe('when the migration can be restarted', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ startedAt: mockedDate, + closedAt: mockedDate, + finishedAt: finishDate, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigrationDO); - schoolMigrationService.unmarkOutdatedUsers.mockResolvedValue(); - userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); + const restartedUserLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ + ...userLoginMigration, + closedAt: undefined, + finishedAt: undefined, + }); + + userLoginMigrationRepo.save.mockResolvedValueOnce(restartedUserLoginMigration); return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, + restartedUserLoginMigration, }; }; - it('should call userLoginMigrationRepo', async () => { - const { schoolId, userLoginMigrationDO } = setup(); + it('should save the migration without closedAt and finishedAt timestamps', async () => { + const { userLoginMigration, restartedUserLoginMigration } = setup(); - await service.restartMigration(schoolId); + await service.restartMigration({ ...userLoginMigration }); - expect(userLoginMigrationRepo.findBySchoolId).toHaveBeenCalledWith(schoolId); - expect(userLoginMigrationRepo.save).toHaveBeenCalledWith(userLoginMigrationDO); + expect(userLoginMigrationRepo.save).toHaveBeenCalledWith(restartedUserLoginMigration); }); - it('should call schoolMigrationService', async () => { - const { schoolId } = setup(); + it('should return the migration', async () => { + const { userLoginMigration, restartedUserLoginMigration } = setup(); - await service.restartMigration(schoolId); + const result: UserLoginMigrationDO = await service.restartMigration({ ...userLoginMigration }); - expect(schoolMigrationService.unmarkOutdatedUsers).toHaveBeenCalledWith(schoolId); + expect(result).toEqual(restartedUserLoginMigration); }); }); - describe('when migration could not be found', () => { + describe('when the migration is still running', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ startedAt: mockedDate, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); + return { + userLoginMigration, + }; + }; + + it('should not save the migration again', async () => { + const { userLoginMigration } = setup(); + + await service.restartMigration({ ...userLoginMigration }); + + expect(userLoginMigrationRepo.save).not.toHaveBeenCalled(); + }); + + it('should return the migration', async () => { + const { userLoginMigration } = setup(); + + const result: UserLoginMigrationDO = await service.restartMigration({ ...userLoginMigration }); + + expect(result).toEqual(userLoginMigration); + }); + }); + + describe('when the grace period for the user login migration is expired', () => { + const setup = () => { + const dateInThePast: Date = new Date(mockedDate.getTime() - 100); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + closedAt: dateInThePast, + finishedAt: dateInThePast, + }); return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, + dateInThePast, }; }; - it('should throw UserLoginMigrationLoggableException ', async () => { - const { schoolId } = setup(); + it('should not save the user login migration again', async () => { + const { userLoginMigration } = setup(); - const func = async () => service.restartMigration(schoolId); + await expect(service.restartMigration({ ...userLoginMigration })).rejects.toThrow(); - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); + expect(userLoginMigrationRepo.save).not.toHaveBeenCalled(); + }); + + it('should return throw an error', async () => { + const { userLoginMigration, dateInThePast } = setup(); + + await expect(service.restartMigration({ ...userLoginMigration })).rejects.toThrow( + new UserLoginMigrationGracePeriodExpiredLoggableException(userLoginMigration.id as string, dateInThePast) + ); }); }); }); @@ -999,7 +624,6 @@ describe('UserLoginMigrationService', () => { describe('closeMigration', () => { describe('when a migration can be closed', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); const userLoginMigration = userLoginMigrationDOFactory.buildWithId(); const closedUserLoginMigration = new UserLoginMigrationDO({ ...userLoginMigration, @@ -1007,60 +631,99 @@ describe('UserLoginMigrationService', () => { finishedAt: finishDate, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); userLoginMigrationRepo.save.mockResolvedValue(closedUserLoginMigration); return { - schoolId, + userLoginMigration, closedUserLoginMigration, }; }; - it('should call schoolService.removeFeature', async () => { - const { schoolId } = setup(); + it('should remove the "ldap sync during migration" school feature', async () => { + const { userLoginMigration } = setup(); - await service.closeMigration(schoolId); + await service.closeMigration({ ...userLoginMigration }); expect(schoolService.removeFeature).toHaveBeenCalledWith( - schoolId, + userLoginMigration.schoolId, SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION ); }); it('should save the closed user login migration', async () => { - const { schoolId, closedUserLoginMigration } = setup(); + const { userLoginMigration, closedUserLoginMigration } = setup(); - await service.closeMigration(schoolId); + await service.closeMigration({ ...userLoginMigration }); expect(userLoginMigrationRepo.save).toHaveBeenCalledWith(closedUserLoginMigration); }); it('should return the closed user login migration', async () => { - const { schoolId, closedUserLoginMigration } = setup(); + const { userLoginMigration, closedUserLoginMigration } = setup(); - const result = await service.closeMigration(schoolId); + const result: UserLoginMigrationDO = await service.closeMigration({ ...userLoginMigration }); expect(result).toEqual(closedUserLoginMigration); }); }); - describe('when a migration can be closed', () => { + describe('when the user login migration was already closed', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + closedAt: mockedDate, + finishedAt: finishDate, + }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); + return { + userLoginMigration, + }; + }; + + it('should not save the user login migration again', async () => { + const { userLoginMigration } = setup(); + + await service.closeMigration({ ...userLoginMigration }); + + expect(userLoginMigrationRepo.save).not.toHaveBeenCalled(); + }); + + it('should return the already closed user login migration', async () => { + const { userLoginMigration } = setup(); + + const result: UserLoginMigrationDO = await service.closeMigration({ ...userLoginMigration }); + + expect(result).toEqual(userLoginMigration); + }); + }); + + describe('when the grace period for the user login migration is expired', () => { + const setup = () => { + const dateInThePast: Date = new Date(mockedDate.getTime() - 100); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + closedAt: dateInThePast, + finishedAt: dateInThePast, + }); return { - schoolId, + userLoginMigration, + dateInThePast, }; }; - it('should save the closed user login migration', async () => { - const { schoolId } = setup(); + it('should not save the user login migration again', async () => { + const { userLoginMigration } = setup(); - const func = () => service.closeMigration(schoolId); + await expect(service.closeMigration({ ...userLoginMigration })).rejects.toThrow(); - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); + expect(userLoginMigrationRepo.save).not.toHaveBeenCalled(); + }); + + it('should return throw an error', async () => { + const { userLoginMigration, dateInThePast } = setup(); + + await expect(service.closeMigration({ ...userLoginMigration })).rejects.toThrow( + new UserLoginMigrationGracePeriodExpiredLoggableException(userLoginMigration.id as string, dateInThePast) + ); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts index 534bb71e104..04f74f8408c 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts @@ -1,12 +1,14 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; -import { UserLoginMigrationRepo } from '@shared/repo'; import { LegacySchoolService } from '@modules/legacy-school'; import { SystemDto, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; -import { UserLoginMigrationNotFoundLoggableException } from '../error'; -import { SchoolMigrationService } from './school-migration.service'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { EntityId, LegacySchoolDo, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { UserLoginMigrationRepo } from '@shared/repo'; +import { + UserLoginMigrationGracePeriodExpiredLoggableException, + UserLoginMigrationNotFoundLoggableException, +} from '../loggable'; @Injectable() export class UserLoginMigrationService { @@ -14,72 +16,10 @@ export class UserLoginMigrationService { private readonly userService: UserService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo, private readonly schoolService: LegacySchoolService, - private readonly systemService: SystemService, - private readonly schoolMigrationService: SchoolMigrationService + private readonly systemService: SystemService ) {} - /** - * @deprecated Use the other functions in this class instead. - * - * @param schoolId - * @param oauthMigrationPossible - * @param oauthMigrationMandatory - * @param oauthMigrationFinished - */ - async setMigration( - schoolId: EntityId, - oauthMigrationPossible?: boolean, - oauthMigrationMandatory?: boolean, - oauthMigrationFinished?: boolean - ): Promise { - const schoolDo: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); - - const existingUserLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId( - schoolId - ); - - let userLoginMigration: UserLoginMigrationDO; - - if (existingUserLoginMigration) { - userLoginMigration = existingUserLoginMigration; - } else { - if (!oauthMigrationPossible) { - throw new UnprocessableEntityException(`School ${schoolId} has no UserLoginMigration`); - } - - userLoginMigration = await this.createNewMigration(schoolDo); - - this.enableOauthMigrationFeature(schoolDo); - await this.schoolService.save(schoolDo); - } - - if (oauthMigrationPossible === true) { - userLoginMigration.closedAt = undefined; - userLoginMigration.finishedAt = undefined; - } - - if (oauthMigrationMandatory !== undefined) { - userLoginMigration.mandatorySince = oauthMigrationMandatory ? new Date() : undefined; - } - - if (oauthMigrationFinished !== undefined) { - userLoginMigration.closedAt = oauthMigrationFinished ? new Date() : undefined; - userLoginMigration.finishedAt = oauthMigrationFinished - ? new Date(Date.now() + (Configuration.get('MIGRATION_END_GRACE_PERIOD_MS') as number)) - : undefined; - } - - const savedMigration: UserLoginMigrationDO = await this.userLoginMigrationRepo.save(userLoginMigration); - - if (oauthMigrationFinished !== undefined) { - // this would throw an error when executed before the userLoginMigrationRepo.save method. - await this.schoolService.removeFeature(schoolId, SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION); - } - - return savedMigration; - } - - async startMigration(schoolId: string): Promise { + public async startMigration(schoolId: string): Promise { const schoolDo: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); const userLoginMigrationDO: UserLoginMigrationDO = await this.createNewMigration(schoolDo); @@ -92,23 +32,23 @@ export class UserLoginMigrationService { return userLoginMigration; } - async restartMigration(schoolId: string): Promise { - const existingUserLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId( - schoolId - ); + public async restartMigration(userLoginMigration: UserLoginMigrationDO): Promise { + this.checkGracePeriod(userLoginMigration); - if (!existingUserLoginMigration) { - throw new UserLoginMigrationNotFoundLoggableException(schoolId); + if (!userLoginMigration.closedAt || !userLoginMigration.finishedAt) { + return userLoginMigration; } - const updatedUserLoginMigration = await this.updateExistingMigration(existingUserLoginMigration); + userLoginMigration.closedAt = undefined; + userLoginMigration.finishedAt = undefined; - await this.schoolMigrationService.unmarkOutdatedUsers(schoolId); + const updatedUserLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationRepo.save(userLoginMigration); return updatedUserLoginMigration; } - async setMigrationMandatory(schoolId: string, mandatory: boolean): Promise { + public async setMigrationMandatory(schoolId: string, mandatory: boolean): Promise { + // this.checkGracePeriod(userLoginMigration); let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); if (!userLoginMigration) { @@ -126,14 +66,17 @@ export class UserLoginMigrationService { return userLoginMigration; } - async closeMigration(schoolId: string): Promise { - let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); + public async closeMigration(userLoginMigration: UserLoginMigrationDO): Promise { + this.checkGracePeriod(userLoginMigration); - if (!userLoginMigration) { - throw new UserLoginMigrationNotFoundLoggableException(schoolId); + if (userLoginMigration.closedAt) { + return userLoginMigration; } - await this.schoolService.removeFeature(schoolId, SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION); + await this.schoolService.removeFeature( + userLoginMigration.schoolId, + SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION + ); const now: Date = new Date(); const gracePeriodDuration: number = Configuration.get('MIGRATION_END_GRACE_PERIOD_MS') as number; @@ -146,6 +89,22 @@ export class UserLoginMigrationService { return userLoginMigration; } + private checkGracePeriod(userLoginMigration: UserLoginMigrationDO) { + if (userLoginMigration.finishedAt && this.isGracePeriodExpired(userLoginMigration)) { + throw new UserLoginMigrationGracePeriodExpiredLoggableException( + userLoginMigration.id as string, + userLoginMigration.finishedAt + ); + } + } + + private isGracePeriodExpired(userLoginMigration: UserLoginMigrationDO): boolean { + const isGracePeriodExpired: boolean = + !!userLoginMigration.finishedAt && Date.now() >= userLoginMigration.finishedAt.getTime(); + + return isGracePeriodExpired; + } + private async createNewMigration(school: LegacySchoolDo): Promise { const oauthSystems: SystemDto[] = await this.systemService.findByType(SystemTypeEnum.OAUTH); const sanisSystem: SystemDto | undefined = oauthSystems.find((system: SystemDto) => system.alias === 'SANIS'); @@ -168,15 +127,6 @@ export class UserLoginMigrationService { return userLoginMigrationDO; } - private async updateExistingMigration(userLoginMigrationDO: UserLoginMigrationDO) { - userLoginMigrationDO.closedAt = undefined; - userLoginMigrationDO.finishedAt = undefined; - - const userLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationRepo.save(userLoginMigrationDO); - - return userLoginMigration; - } - private enableOauthMigrationFeature(schoolDo: LegacySchoolDo) { if (schoolDo.features && !schoolDo.features.includes(SchoolFeatures.OAUTH_PROVISIONING_ENABLED)) { schoolDo.features.push(SchoolFeatures.OAUTH_PROVISIONING_ENABLED); @@ -185,13 +135,13 @@ export class UserLoginMigrationService { } } - async findMigrationBySchool(schoolId: string): Promise { + public async findMigrationBySchool(schoolId: string): Promise { const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); return userLoginMigration; } - async findMigrationByUser(userId: EntityId): Promise { + public async findMigrationByUser(userId: EntityId): Promise { const userDO: UserDO = await this.userService.findById(userId); const { schoolId } = userDO; @@ -211,7 +161,7 @@ export class UserLoginMigrationService { return userLoginMigration; } - async deleteUserLoginMigration(userLoginMigration: UserLoginMigrationDO): Promise { + public async deleteUserLoginMigration(userLoginMigration: UserLoginMigrationDO): Promise { await this.userLoginMigrationRepo.delete(userLoginMigration); } } diff --git a/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts index c098066664d..e738a1feac3 100644 --- a/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts @@ -1,55 +1,27 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BadRequestException, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, RoleName, UserDO } from '@shared/domain'; -import { legacySchoolDoFactory, setupEntities, userDoFactory } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { AccountService } from '@modules/account/services/account.service'; -import { AccountDto, AccountSaveDto } from '@modules/account/services/dto'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { SystemService } from '@modules/system'; -import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { AccountDto } from '@modules/account/services/dto'; import { UserService } from '@modules/user'; -import { PageTypes } from '../interface/page-types.enum'; -import { PageContentDto } from './dto'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UserDO } from '@shared/domain'; +import { roleFactory, setupEntities, userDoFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; +import { UserMigrationDatabaseOperationFailedLoggableException } from '../loggable'; import { UserMigrationService } from './user-migration.service'; -describe('UserMigrationService', () => { +describe(UserMigrationService.name, () => { let module: TestingModule; let service: UserMigrationService; - let configBefore: IConfig; - let logger: LegacyLogger; - let schoolService: DeepMocked; - let systemService: DeepMocked; let userService: DeepMocked; let accountService: DeepMocked; - - const hostUri = 'http://this.de'; - const apiUrl = 'http://mock.de'; - const s3 = 'sKey123456789123456789'; + let logger: DeepMocked; beforeAll(async () => { - configBefore = Configuration.toObject({ plainSecrets: true }); - Configuration.set('HOST', hostUri); - Configuration.set('PUBLIC_BACKEND_URL', apiUrl); - Configuration.set('S3_KEY', s3); - module = await Test.createTestingModule({ providers: [ UserMigrationService, - { - provide: LegacySchoolService, - useValue: createMock(), - }, - { - provide: SystemService, - useValue: createMock(), - }, { provide: UserService, useValue: createMock(), @@ -59,398 +31,204 @@ describe('UserMigrationService', () => { useValue: createMock(), }, { - provide: LegacyLogger, - useValue: createMock(), + provide: Logger, + useValue: createMock(), }, ], }).compile(); service = module.get(UserMigrationService); - schoolService = module.get(LegacySchoolService); - systemService = module.get(SystemService); userService = module.get(UserService); accountService = module.get(AccountService); - logger = module.get(LegacyLogger); + logger = module.get(Logger); await setupEntities(); }); afterAll(async () => { await module.close(); - - Configuration.reset(configBefore); }); afterEach(() => { jest.resetAllMocks(); }); - describe('getMigrationConsentPageRedirect is called', () => { - describe('when finding the migration systems', () => { - const setup = () => { - const officialSchoolNumber = '3'; - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ name: 'schoolName', officialSchoolNumber }); - - schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); - - return { - officialSchoolNumber, - }; - }; + describe('migrateUser', () => { + const mockDate = new Date(2020, 1, 1); - it('should return a url to the migration endpoint', async () => { - const { officialSchoolNumber } = setup(); - - const result: string = await service.getMigrationConsentPageRedirect(officialSchoolNumber, 'iservId'); - - expect(result).toEqual('http://this.de/migration?origin=iservId'); - }); + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(mockDate); }); - describe('when the school was not found', () => { + describe('when migrate user was successful', () => { const setup = () => { - const officialSchoolNumber = '3'; + const targetSystemId = new ObjectId().toHexString(); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(null); + const role = roleFactory.buildWithId(); + const userId = new ObjectId().toHexString(); + const targetExternalId = 'newUserExternalId'; + const sourceExternalId = 'currentUserExternalId'; + const user: UserDO = userDoFactory.buildWithId({ + id: userId, + createdAt: mockDate, + updatedAt: mockDate, + email: 'emailMock', + firstName: 'firstNameMock', + lastName: 'lastNameMock', + schoolId: 'schoolMock', + roles: [role], + externalId: sourceExternalId, + }); + + const accountId = new ObjectId().toHexString(); + const sourceSystemId = new ObjectId().toHexString(); + const accountDto: AccountDto = new AccountDto({ + id: accountId, + updatedAt: new Date(), + createdAt: new Date(), + userId, + username: '', + systemId: sourceSystemId, + }); + + userService.findById.mockResolvedValueOnce({ ...user }); + accountService.findByUserIdOrFail.mockResolvedValueOnce({ ...accountDto }); return { - officialSchoolNumber, + user, + userId, + targetExternalId, + sourceExternalId, + accountDto, + sourceSystemId, + targetSystemId, }; }; - it('should throw InternalServerErrorException', async () => { - const { officialSchoolNumber } = setup(); - schoolService.getSchoolBySchoolNumber.mockResolvedValue(null); + it('should use the correct user', async () => { + const { userId, targetExternalId, targetSystemId } = setup(); - const promise: Promise = service.getMigrationConsentPageRedirect(officialSchoolNumber, 'systemId'); + await service.migrateUser(userId, targetExternalId, targetSystemId); - await expect(promise).rejects.toThrow(NotFoundException); + expect(userService.findById).toHaveBeenCalledWith(userId); }); - }); - }); - describe('getMigrationRedirectUri is called', () => { - describe('when a Redirect-URL for a system is requested', () => { - it('should return a proper redirect', () => { - const response = service.getMigrationRedirectUri(); + it('should use the correct account', async () => { + const { userId, targetExternalId, targetSystemId } = setup(); - expect(response).toContain('migration'); - }); - }); - }); + await service.migrateUser(userId, targetExternalId, targetSystemId); - describe('getPageContent is called', () => { - const setupPageContent = () => { - const sourceOauthConfig: OauthConfigDto = new OauthConfigDto({ - clientId: 'sourceClientId', - clientSecret: 'sourceSecret', - tokenEndpoint: 'http://source.de/auth/public/mockToken', - grantType: 'authorization_code', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'http://source.de/auth', - provider: 'source_provider', - logoutEndpoint: 'source_logoutEndpoint', - issuer: 'source_issuer', - jwksEndpoint: 'source_jwksEndpoint', - redirectUri: 'http://this.de/api/v3/sso/oauth/sourceSystemId', - }); - const targetOauthConfig: OauthConfigDto = new OauthConfigDto({ - clientId: 'targetClientId', - clientSecret: 'targetSecret', - tokenEndpoint: 'http://target.de/auth/public/mockToken', - grantType: 'authorization_code', - scope: 'openid uuid', - responseType: 'code', - authEndpoint: 'http://target.de/auth', - provider: 'target_provider', - logoutEndpoint: 'target_logoutEndpoint', - issuer: 'target_issuer', - jwksEndpoint: 'target_jwksEndpoint', - redirectUri: 'http://this.de/api/v3/sso/oauth/targetSystemId', - }); - const sourceSystem: SystemDto = new SystemDto({ - id: 'sourceSystemId', - type: 'oauth', - alias: 'Iserv', - oauthConfig: sourceOauthConfig, - }); - const targetSystem: SystemDto = new SystemDto({ - id: 'targetSystemId', - type: 'oauth', - alias: 'Sanis', - oauthConfig: targetOauthConfig, + expect(accountService.findByUserIdOrFail).toHaveBeenCalledWith(userId); }); - const migrationRedirectUri = 'http://mock.de/api/v3/sso/oauth/targetSystemId/migration'; + it('should save the migrated user', async () => { + const { userId, targetExternalId, targetSystemId, user, sourceExternalId } = setup(); - return { sourceSystem, targetSystem, sourceOauthConfig, targetOauthConfig, migrationRedirectUri }; - }; + await service.migrateUser(userId, targetExternalId, targetSystemId); - describe('when coming from the target system', () => { - it('should return the url to the source system and a frontpage url', async () => { - const { sourceSystem, targetSystem } = setupPageContent(); - const sourceSystemLoginUrl = `http://mock.de/api/v3/sso/login/sourceSystemId?postLoginRedirect=http%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Flogin%2FtargetSystemId%3Fmigration%3Dtrue`; - - systemService.findById.mockResolvedValueOnce(sourceSystem); - systemService.findById.mockResolvedValueOnce(targetSystem); - - const contentDto: PageContentDto = await service.getPageContent( - PageTypes.START_FROM_TARGET_SYSTEM, - sourceSystem.id as string, - targetSystem.id as string - ); - - expect(contentDto).toEqual({ - proceedButtonUrl: sourceSystemLoginUrl, - cancelButtonUrl: '/login', + expect(userService.save).toHaveBeenCalledWith({ + ...user, + externalId: targetExternalId, + previousExternalId: sourceExternalId, + lastLoginSystemChange: mockDate, }); }); - }); - - describe('when coming from the source system', () => { - it('should return the url to the target system and a dashboard url', async () => { - const { sourceSystem, targetSystem } = setupPageContent(); - const targetSystemLoginUrl = `http://mock.de/api/v3/sso/login/targetSystemId?migration=true`; - systemService.findById.mockResolvedValueOnce(sourceSystem); - systemService.findById.mockResolvedValueOnce(targetSystem); + it('should save the migrated account', async () => { + const { userId, targetExternalId, targetSystemId, accountDto } = setup(); - const contentDto: PageContentDto = await service.getPageContent( - PageTypes.START_FROM_SOURCE_SYSTEM, - sourceSystem.id as string, - targetSystem.id as string - ); + await service.migrateUser(userId, targetExternalId, targetSystemId); - expect(contentDto).toEqual({ - proceedButtonUrl: targetSystemLoginUrl, - cancelButtonUrl: '/dashboard', + expect(accountService.save).toHaveBeenCalledWith({ + ...accountDto, + systemId: targetSystemId, }); }); }); - describe('when coming from the source system and the migration is mandatory', () => { - it('should return the url to the target system and a logout url', async () => { - const { sourceSystem, targetSystem } = setupPageContent(); - const targetSystemLoginUrl = `http://mock.de/api/v3/sso/login/targetSystemId?migration=true`; - - systemService.findById.mockResolvedValueOnce(sourceSystem); - systemService.findById.mockResolvedValueOnce(targetSystem); - - const contentDto: PageContentDto = await service.getPageContent( - PageTypes.START_FROM_SOURCE_SYSTEM_MANDATORY, - sourceSystem.id as string, - targetSystem.id as string - ); + describe('when saving to the database fails', () => { + const setup = () => { + const targetSystemId = new ObjectId().toHexString(); - expect(contentDto).toEqual({ - proceedButtonUrl: targetSystemLoginUrl, - cancelButtonUrl: '/logout', + const role = roleFactory.buildWithId(); + const userId = new ObjectId().toHexString(); + const targetExternalId = 'newUserExternalId'; + const sourceExternalId = 'currentUserExternalId'; + const user: UserDO = userDoFactory.buildWithId({ + id: userId, + createdAt: mockDate, + updatedAt: mockDate, + email: 'emailMock', + firstName: 'firstNameMock', + lastName: 'lastNameMock', + schoolId: 'schoolMock', + roles: [role], + externalId: sourceExternalId, }); - }); - }); - describe('when a wrong page type is given', () => { - it('throws a BadRequestException', async () => { - const { sourceSystem, targetSystem } = setupPageContent(); - systemService.findById.mockResolvedValueOnce(sourceSystem); - systemService.findById.mockResolvedValueOnce(targetSystem); - - const promise: Promise = service.getPageContent('undefined' as PageTypes, '', ''); - - await expect(promise).rejects.toThrow(BadRequestException); - }); - }); - - describe('when a system has no oauth config', () => { - it('throws a EntityNotFoundError', async () => { - const { sourceSystem, targetSystem } = setupPageContent(); - sourceSystem.oauthConfig = undefined; - systemService.findById.mockResolvedValueOnce(sourceSystem); - systemService.findById.mockResolvedValueOnce(targetSystem); - - const promise: Promise = service.getPageContent( - PageTypes.START_FROM_TARGET_SYSTEM, - 'invalid', - 'invalid' - ); - - await expect(promise).rejects.toThrow(UnprocessableEntityException); - }); - }); - }); - - describe('migrateUser is called', () => { - beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(2020, 1, 1)); - }); - - const setupMigrationData = () => { - const targetSystemId = new ObjectId().toHexString(); - - const notMigratedUser: UserDO = userDoFactory - .withRoles([{ id: 'roleIdMock', name: RoleName.STUDENT }]) - .buildWithId( - { - createdAt: new Date(), - updatedAt: new Date(), - email: 'emailMock', - firstName: 'firstNameMock', - lastName: 'lastNameMock', - schoolId: 'schoolMock', - externalId: 'currentUserExternalIdMock', - }, - 'userId' - ); + const accountId = new ObjectId().toHexString(); + const sourceSystemId = new ObjectId().toHexString(); + const accountDto: AccountDto = new AccountDto({ + id: accountId, + updatedAt: new Date(), + createdAt: new Date(), + userId, + username: '', + systemId: sourceSystemId, + }); - const migratedUserDO: UserDO = userDoFactory - .withRoles([{ id: 'roleIdMock', name: RoleName.STUDENT }]) - .buildWithId( - { - createdAt: new Date(), - updatedAt: new Date(), - email: 'emailMock', - firstName: 'firstNameMock', - lastName: 'lastNameMock', - schoolId: 'schoolMock', - externalId: 'externalUserTargetId', - previousExternalId: 'currentUserExternalIdMock', - lastLoginSystemChange: new Date(), - }, - 'userId' - ); + const error = new Error('Cannot save'); - const accountId = new ObjectId().toHexString(); - const userId = new ObjectId().toHexString(); - const sourceSystemId = new ObjectId().toHexString(); - - const accountDto: AccountDto = new AccountDto({ - id: accountId, - updatedAt: new Date(), - createdAt: new Date(), - userId, - username: '', - systemId: sourceSystemId, - }); + userService.findById.mockResolvedValueOnce({ ...user }); + accountService.findByUserIdOrFail.mockResolvedValueOnce({ ...accountDto }); - const migratedAccount: AccountSaveDto = new AccountSaveDto({ - id: accountId, - updatedAt: new Date(), - createdAt: new Date(), - userId, - username: '', - systemId: targetSystemId, - }); + userService.save.mockRejectedValueOnce(error); + accountService.save.mockRejectedValueOnce(error); - return { - accountDto, - migratedUserDO, - notMigratedUser, - migratedAccount, - sourceSystemId, - targetSystemId, + return { + user, + userId, + targetExternalId, + sourceExternalId, + accountDto, + sourceSystemId, + targetSystemId, + error, + }; }; - }; - - describe('when migrate user was successful', () => { - it('should return to migration succeed page', async () => { - const { targetSystemId, sourceSystemId, accountDto } = setupMigrationData(); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); - - const result = await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); - - expect(result.redirect).toStrictEqual( - `${hostUri}/migration/success?sourceSystem=${sourceSystemId}&targetSystem=${targetSystemId}` - ); - }); - it('should call methods of migration ', async () => { - const { migratedUserDO, migratedAccount, targetSystemId, notMigratedUser, accountDto } = setupMigrationData(); - userService.findById.mockResolvedValue(notMigratedUser); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); + it('should roll back possible changes to the user', async () => { + const { userId, targetExternalId, targetSystemId, user } = setup(); - await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); + await expect(service.migrateUser(userId, targetExternalId, targetSystemId)).rejects.toThrow(); - expect(userService.findById).toHaveBeenCalledWith('userId'); - expect(userService.save).toHaveBeenCalledWith(migratedUserDO); - expect(accountService.findByUserIdOrFail).toHaveBeenCalledWith('userId'); - expect(accountService.save).toHaveBeenCalledWith(migratedAccount); + expect(userService.save).toHaveBeenLastCalledWith(user); }); - it('should do migration of user', async () => { - const { migratedUserDO, notMigratedUser, accountDto, targetSystemId } = setupMigrationData(); - userService.findById.mockResolvedValue(notMigratedUser); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); + it('should roll back possible changes to the account', async () => { + const { userId, targetExternalId, targetSystemId, accountDto } = setup(); - await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); + await expect(service.migrateUser(userId, targetExternalId, targetSystemId)).rejects.toThrow(); - expect(userService.save).toHaveBeenCalledWith(migratedUserDO); + expect(accountService.save).toHaveBeenLastCalledWith(accountDto); }); - it('should do migration of account', async () => { - const { notMigratedUser, accountDto, migratedAccount, targetSystemId } = setupMigrationData(); - userService.findById.mockResolvedValue(notMigratedUser); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); - - await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); + it('should log a rollback error', async () => { + const { userId, targetExternalId, targetSystemId, error } = setup(); - expect(accountService.save).toHaveBeenCalledWith(migratedAccount); - }); - }); - - describe('when migration step failed', () => { - it('should throw Error', async () => { - const targetSystemId = new ObjectId().toHexString(); - userService.findById.mockRejectedValue(new NotFoundException('Could not find User')); + await expect(service.migrateUser(userId, targetExternalId, targetSystemId)).rejects.toThrow(); - await expect(service.migrateUser('userId', 'externalUserTargetId', targetSystemId)).rejects.toThrow( - new NotFoundException('Could not find User') + expect(logger.warning).toHaveBeenCalledWith( + new UserMigrationDatabaseOperationFailedLoggableException(userId, 'rollback', error) ); }); - it('should log error and message', async () => { - const { migratedUserDO, accountDto, targetSystemId } = setupMigrationData(); - const error = new NotFoundException('Test Error'); - userService.findById.mockResolvedValue(migratedUserDO); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); - accountService.save.mockRejectedValueOnce(error); - - await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); - - expect(logger.log).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'This error occurred during migration of User:', - affectedUserId: 'userId', - error, - }) - ); - }); - - it('should do a rollback of migration', async () => { - const { notMigratedUser, accountDto, targetSystemId } = setupMigrationData(); - const error = new NotFoundException('Test Error'); - userService.findById.mockResolvedValue(notMigratedUser); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); - accountService.save.mockRejectedValueOnce(error); - - await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); - - expect(userService.save).toHaveBeenCalledWith(notMigratedUser); - expect(accountService.save).toHaveBeenCalledWith(accountDto); - }); - - it('should return to dashboard', async () => { - const { migratedUserDO, accountDto, targetSystemId, sourceSystemId } = setupMigrationData(); - const error = new NotFoundException('Test Error'); - userService.findById.mockResolvedValue(migratedUserDO); - accountService.findByUserIdOrFail.mockResolvedValue(accountDto); - accountService.save.mockRejectedValueOnce(error); - - const result = await service.migrateUser('userId', 'externalUserTargetId', targetSystemId); + it('should throw an error', async () => { + const { userId, targetExternalId, targetSystemId, error } = setup(); - expect(result.redirect).toStrictEqual( - `${hostUri}/migration/error?sourceSystem=${sourceSystemId}&targetSystem=${targetSystemId}` + await expect(service.migrateUser(userId, targetExternalId, targetSystemId)).rejects.toThrow( + new UserMigrationDatabaseOperationFailedLoggableException(userId, 'migration', error) ); }); }); diff --git a/apps/server/src/modules/user-login-migration/service/user-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-migration.service.ts index c9a6e8648c8..38c020b43cb 100644 --- a/apps/server/src/modules/user-login-migration/service/user-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-migration.service.ts @@ -1,144 +1,42 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { BadRequestException, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { LegacySchoolDo } from '@shared/domain'; -import { UserDO } from '@shared/domain/domainobject/user.do'; -import { LegacyLogger } from '@src/core/logger'; import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { SystemDto, SystemService } from '@modules/system/service'; import { UserService } from '@modules/user'; -import { EntityId } from '@src/shared/domain/types'; -import { PageTypes } from '../interface/page-types.enum'; -import { MigrationDto } from './dto/migration.dto'; -import { PageContentDto } from './dto/page-content.dto'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { UserDO } from '@shared/domain/domainobject/user.do'; +import { Logger } from '@src/core/logger'; +import { UserMigrationDatabaseOperationFailedLoggableException } from '../loggable'; @Injectable() -/** - * @deprecated - */ export class UserMigrationService { - private readonly hostUrl: string; - - private readonly publicBackendUrl: string; - - private readonly dashboardUrl: string = '/dashboard'; - - private readonly logoutUrl: string = '/logout'; - - private readonly loginUrl: string = '/login'; - constructor( - private readonly schoolService: LegacySchoolService, - private readonly systemService: SystemService, private readonly userService: UserService, - private readonly logger: LegacyLogger, - private readonly accountService: AccountService - ) { - this.hostUrl = Configuration.get('HOST') as string; - this.publicBackendUrl = Configuration.get('PUBLIC_BACKEND_URL') as string; - } - - async getMigrationConsentPageRedirect(officialSchoolNumber: string, originSystemId: string): Promise { - const school: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); - - if (!school || !school.id) { - throw new NotFoundException(`School with offical school number ${officialSchoolNumber} does not exist.`); - } - - const url = new URL('/migration', this.hostUrl); - url.searchParams.append('origin', originSystemId); - return url.toString(); - } - - async getPageContent(pageType: PageTypes, sourceId: string, targetId: string): Promise { - const sourceSystem: SystemDto = await this.systemService.findById(sourceId); - const targetSystem: SystemDto = await this.systemService.findById(targetId); - - const targetSystemLoginUrl: string = this.getLoginUrl(targetSystem); - - switch (pageType) { - case PageTypes.START_FROM_TARGET_SYSTEM: { - const sourceSystemLoginUrl: string = this.getLoginUrl(sourceSystem, targetSystemLoginUrl.toString()); - - const content: PageContentDto = new PageContentDto({ - proceedButtonUrl: sourceSystemLoginUrl.toString(), - cancelButtonUrl: this.loginUrl, - }); - return content; - } - case PageTypes.START_FROM_SOURCE_SYSTEM: { - const content: PageContentDto = new PageContentDto({ - proceedButtonUrl: targetSystemLoginUrl.toString(), - cancelButtonUrl: this.dashboardUrl, - }); - return content; - } - case PageTypes.START_FROM_SOURCE_SYSTEM_MANDATORY: { - const content: PageContentDto = new PageContentDto({ - proceedButtonUrl: targetSystemLoginUrl.toString(), - cancelButtonUrl: this.logoutUrl, - }); - return content; - } - default: { - throw new BadRequestException('Unknown PageType requested'); - } - } - } + private readonly accountService: AccountService, + private readonly logger: Logger + ) {} - // TODO: https://ticketsystem.dbildungscloud.de/browse/N21-632 Move Redirect Logic URLs to Client - getMigrationRedirectUri(): string { - const combinedUri = new URL(this.publicBackendUrl); - combinedUri.pathname = `api/v3/sso/oauth/migration`; - return combinedUri.toString(); - } - - async migrateUser(currentUserId: string, externalUserId: string, targetSystemId: string): Promise { + async migrateUser(currentUserId: EntityId, externalUserId: string, targetSystemId: EntityId): Promise { const userDO: UserDO = await this.userService.findById(currentUserId); const account: AccountDto = await this.accountService.findByUserIdOrFail(currentUserId); + const userDOCopy: UserDO = new UserDO({ ...userDO }); const accountCopy: AccountDto = new AccountDto({ ...account }); - let migrationDto: MigrationDto; try { - migrationDto = await this.doMigration(userDO, externalUserId, account, targetSystemId, accountCopy.systemId); - } catch (e: unknown) { - this.logger.log({ - message: 'This error occurred during migration of User:', - affectedUserId: currentUserId, - error: e, - }); + await this.doMigration(userDO, externalUserId, account, targetSystemId); + } catch (error: unknown) { + await this.tryRollbackMigration(currentUserId, userDOCopy, accountCopy); - migrationDto = await this.rollbackMigration(userDOCopy, accountCopy, targetSystemId); + throw new UserMigrationDatabaseOperationFailedLoggableException(currentUserId, 'migration', error); } - - return migrationDto; - } - - private async rollbackMigration( - userDOCopy: UserDO, - accountCopy: AccountDto, - targetSystemId: string - ): Promise { - await this.userService.save(userDOCopy); - await this.accountService.save(accountCopy); - - const userMigrationDto: MigrationDto = this.createUserMigrationDto( - '/migration/error', - accountCopy.systemId ?? '', - targetSystemId - ); - return userMigrationDto; } private async doMigration( userDO: UserDO, externalUserId: string, account: AccountDto, - targetSystemId: string, - accountId?: EntityId - ): Promise { + targetSystemId: string + ): Promise { userDO.previousExternalId = userDO.externalId; userDO.externalId = externalUserId; userDO.lastLoginSystemChange = new Date(); @@ -146,38 +44,18 @@ export class UserMigrationService { account.systemId = targetSystemId; await this.accountService.save(account); - - const userMigrationDto: MigrationDto = this.createUserMigrationDto( - '/migration/success', - accountId ?? '', - targetSystemId - ); - return userMigrationDto; } - // TODO: https://ticketsystem.dbildungscloud.de/browse/N21-632 Move Redirect Logic URLs to Client - private createUserMigrationDto(urlPath: string, sourceSystemId: string, targetSystemId: string) { - const errorUrl: URL = new URL(urlPath, this.hostUrl); - errorUrl.searchParams.append('sourceSystem', sourceSystemId); - errorUrl.searchParams.append('targetSystem', targetSystemId); - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: errorUrl.toString(), - }); - return userMigrationDto; - } - - private getLoginUrl(system: SystemDto, postLoginRedirect?: string): string { - if (!system.oauthConfig || !system.id) { - throw new UnprocessableEntityException(`System ${system?.id || 'unknown'} has no oauth config`); - } - - const loginUrl: URL = new URL(`api/v3/sso/login/${system.id}`, this.publicBackendUrl); - if (postLoginRedirect) { - loginUrl.searchParams.append('postLoginRedirect', postLoginRedirect); - } else { - loginUrl.searchParams.append('migration', 'true'); + private async tryRollbackMigration( + currentUserId: EntityId, + userDOCopy: UserDO, + accountCopy: AccountDto + ): Promise { + try { + await this.userService.save(userDOCopy); + await this.accountService.save(accountCopy); + } catch (error: unknown) { + this.logger.warning(new UserMigrationDatabaseOperationFailedLoggableException(currentUserId, 'rollback', error)); } - - return loginUrl.toString(); } } diff --git a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts index b14ab751d40..a3eead10096 100644 --- a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts @@ -1,13 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, UserLoginMigrationDO } from '@shared/domain'; import { setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; -import { Action, AuthorizationService } from '@modules/authorization'; -import { UserLoginMigrationNotFoundLoggableException } from '../error'; +import { UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService } from '../service'; import { CloseUserLoginMigrationUc } from './close-user-login-migration.uc'; -describe('CloseUserLoginMigrationUc', () => { +describe(CloseUserLoginMigrationUc.name, () => { let module: TestingModule; let uc: CloseUserLoginMigrationUc; @@ -65,12 +66,12 @@ describe('CloseUserLoginMigrationUc', () => { ...userLoginMigration, closedAt: new Date(2023, 1), }); - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authorizationService.getUserWithPermissions.mockResolvedValue(user); - userLoginMigrationService.closeMigration.mockResolvedValue(closedUserLoginMigration); - schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(true); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(userLoginMigration); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userLoginMigrationService.closeMigration.mockResolvedValueOnce(closedUserLoginMigration); + schoolMigrationService.hasSchoolMigratedUser.mockResolvedValueOnce(true); return { user, @@ -85,26 +86,27 @@ describe('CloseUserLoginMigrationUc', () => { await uc.closeMigration(user.id, schoolId); - expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, userLoginMigration, { - requiredPermissions: [Permission.USER_LOGIN_MIGRATION_ADMIN], - action: Action.write, - }); + expect(authorizationService.checkPermission).toHaveBeenCalledWith( + user, + userLoginMigration, + AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) + ); }); it('should close the migration', async () => { - const { user, schoolId } = setup(); + const { user, schoolId, userLoginMigration } = setup(); await uc.closeMigration(user.id, schoolId); - expect(userLoginMigrationService.closeMigration).toHaveBeenCalledWith(schoolId); + expect(userLoginMigrationService.closeMigration).toHaveBeenCalledWith(userLoginMigration); }); it('should mark all un-migrated users as outdated', async () => { - const { user, schoolId } = setup(); + const { user, schoolId, closedUserLoginMigration } = setup(); await uc.closeMigration(user.id, schoolId); - expect(schoolMigrationService.markUnmigratedUsersAsOutdated).toHaveBeenCalledWith(schoolId); + expect(schoolMigrationService.markUnmigratedUsersAsOutdated).toHaveBeenCalledWith(closedUserLoginMigration); }); it('should return the closed user login migration', async () => { @@ -119,9 +121,9 @@ describe('CloseUserLoginMigrationUc', () => { describe('when no user login migration exists', () => { const setup = () => { const user = userFactory.buildWithId(); - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(null); return { user, @@ -148,10 +150,10 @@ describe('CloseUserLoginMigrationUc', () => { }); const schoolId = 'schoolId'; - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authorizationService.getUserWithPermissions.mockResolvedValue(user); - userLoginMigrationService.closeMigration.mockResolvedValue(closedUserLoginMigration); - schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(false); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(userLoginMigration); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userLoginMigrationService.closeMigration.mockResolvedValueOnce(closedUserLoginMigration); + schoolMigrationService.hasSchoolMigratedUser.mockResolvedValueOnce(false); return { user, @@ -166,10 +168,11 @@ describe('CloseUserLoginMigrationUc', () => { await uc.closeMigration(user.id, schoolId); - expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, userLoginMigration, { - requiredPermissions: [Permission.USER_LOGIN_MIGRATION_ADMIN], - action: Action.write, - }); + expect(authorizationService.checkPermission).toHaveBeenCalledWith( + user, + userLoginMigration, + AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) + ); }); it('should revert the start of the migration', async () => { @@ -188,7 +191,7 @@ describe('CloseUserLoginMigrationUc', () => { expect(schoolMigrationService.markUnmigratedUsersAsOutdated).not.toHaveBeenCalled(); }); - it('should return undefined', async () => { + it('should return undefined', async () => { const { user, schoolId } = setup(); const result = await uc.closeMigration(user.id, schoolId); @@ -196,41 +199,5 @@ describe('CloseUserLoginMigrationUc', () => { expect(result).toBeUndefined(); }); }); - - describe('when the user login migration was already closed', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ - closedAt: new Date(2023, 1), - }); - const schoolId = 'schoolId'; - - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authorizationService.getUserWithPermissions.mockResolvedValue(user); - schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(false); - - return { - user, - schoolId, - userLoginMigration, - }; - }; - - it('should not modify the user login migration', async () => { - const { user, schoolId } = setup(); - - await uc.closeMigration(user.id, schoolId); - - expect(userLoginMigrationService.closeMigration).not.toHaveBeenCalled(); - }); - - it('should return the already closed user login migration', async () => { - const { user, schoolId, userLoginMigration } = setup(); - - const result = await uc.closeMigration(user.id, schoolId); - - expect(result).toEqual(userLoginMigration); - }); - }); }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts index 65bdad24782..c04064f813a 100644 --- a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts @@ -1,10 +1,7 @@ +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { EntityId, Permission, User, UserLoginMigrationDO } from '@shared/domain'; -import { Action, AuthorizationService } from '@modules/authorization'; -import { - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, -} from '../error'; +import { UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService } from '../service'; @Injectable() @@ -26,39 +23,26 @@ export class CloseUserLoginMigrationUc { } const user: User = await this.authorizationService.getUserWithPermissions(userId); - this.authorizationService.checkPermission(user, userLoginMigration, { - requiredPermissions: [Permission.USER_LOGIN_MIGRATION_ADMIN], - action: Action.write, - }); + this.authorizationService.checkPermission( + user, + userLoginMigration, + AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) + ); - if (userLoginMigration.finishedAt && this.isGracePeriodExpired(userLoginMigration)) { - throw new UserLoginMigrationGracePeriodExpiredLoggableException( - userLoginMigration.id as string, - userLoginMigration.finishedAt - ); - } else if (userLoginMigration.closedAt) { - return userLoginMigration; - } else { - const updatedUserLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationService.closeMigration( - schoolId - ); + const updatedUserLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationService.closeMigration( + userLoginMigration + ); - const hasSchoolMigratedUser: boolean = await this.schoolMigrationService.hasSchoolMigratedUser(schoolId); + const hasSchoolMigratedUser: boolean = await this.schoolMigrationService.hasSchoolMigratedUser(schoolId); - if (!hasSchoolMigratedUser) { - await this.userLoginMigrationRevertService.revertUserLoginMigration(updatedUserLoginMigration); - return undefined; - } - await this.schoolMigrationService.markUnmigratedUsersAsOutdated(schoolId); + if (!hasSchoolMigratedUser) { + await this.userLoginMigrationRevertService.revertUserLoginMigration(updatedUserLoginMigration); - return updatedUserLoginMigration; + return undefined; } - } - private isGracePeriodExpired(userLoginMigration: UserLoginMigrationDO): boolean { - const isGracePeriodExpired: boolean = - !!userLoginMigration.finishedAt && Date.now() >= userLoginMigration.finishedAt.getTime(); + await this.schoolMigrationService.markUnmigratedUsersAsOutdated(updatedUserLoginMigration); - return isGracePeriodExpired; + return updatedUserLoginMigration; } } diff --git a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts index dd4ac4f835b..1aace730ee4 100644 --- a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts @@ -1,25 +1,21 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, -} from '../error'; -import { UserLoginMigrationService } from '../service'; +import { UserLoginMigrationNotFoundLoggableException } from '../loggable'; +import { SchoolMigrationService, UserLoginMigrationService } from '../service'; import { RestartUserLoginMigrationUc } from './restart-user-login-migration.uc'; -describe('RestartUserLoginMigrationUc', () => { +describe(RestartUserLoginMigrationUc.name, () => { let module: TestingModule; let uc: RestartUserLoginMigrationUc; let userLoginMigrationService: DeepMocked; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let schoolMigrationService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -34,8 +30,8 @@ describe('RestartUserLoginMigrationUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolMigrationService, + useValue: createMock(), }, { provide: Logger, @@ -47,7 +43,7 @@ describe('RestartUserLoginMigrationUc', () => { uc = module.get(RestartUserLoginMigrationUc); userLoginMigrationService = module.get(UserLoginMigrationService); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + schoolMigrationService = module.get(SchoolMigrationService); await setupEntities(); }); @@ -66,112 +62,86 @@ describe('RestartUserLoginMigrationUc', () => { const migrationBeforeRestart: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ closedAt: new Date(2023, 5), }); - const migrationAfterRestart: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId(); + const migrationAfterRestart: UserLoginMigrationDO = new UserLoginMigrationDO({ + ...migrationBeforeRestart, + closedAt: undefined, + }); const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migrationBeforeRestart); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); userLoginMigrationService.restartMigration.mockResolvedValueOnce(migrationAfterRestart); - return { user, school, migrationAfterRestart }; + return { + user, + migrationBeforeRestart, + migrationAfterRestart, + }; }; it('should check permission', async () => { - const { user, school } = setup(); + const { user, migrationBeforeRestart } = setup(); - await uc.restartMigration('userId', 'schoolId'); + await uc.restartMigration(user.id, user.school.id); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, - school, + migrationBeforeRestart, AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) ); }); - it('should call the service to restart a migration', async () => { - setup(); - - await uc.restartMigration('userId', 'schoolId'); - - expect(userLoginMigrationService.restartMigration).toHaveBeenCalledWith('schoolId'); - }); - - it('should return a UserLoginMigration', async () => { - const { migrationAfterRestart } = setup(); - - const result: UserLoginMigrationDO = await uc.restartMigration('userId', 'schoolId'); + it('should restart the migration', async () => { + const { user, migrationBeforeRestart } = setup(); - expect(result).toEqual(migrationAfterRestart); - }); - }); + await uc.restartMigration(user.id, user.school.id); - describe('when an admin restarts a running migration', () => { - const setup = () => { - const runningMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId(); - - const user: User = userFactory.buildWithId(); - - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(runningMigration); - - return { user, school, runningMigration }; - }; - - it('should check permission', async () => { - const { user, school } = setup(); - - await uc.restartMigration('userId', 'schoolId'); - - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - school, - AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) - ); + expect(userLoginMigrationService.restartMigration).toHaveBeenCalledWith(migrationBeforeRestart); }); - it('should not call the service to restart a migration', async () => { - setup(); + it('should unmark outdated users', async () => { + const { user, migrationAfterRestart } = setup(); - await uc.restartMigration('userId', 'schoolId'); + await uc.restartMigration(user.id, user.school.id); - expect(userLoginMigrationService.restartMigration).not.toHaveBeenCalled(); + expect(schoolMigrationService.unmarkOutdatedUsers).toHaveBeenCalledWith(migrationAfterRestart); }); it('should return a UserLoginMigration', async () => { - const { runningMigration } = setup(); + const { user, migrationAfterRestart } = setup(); - const result: UserLoginMigrationDO = await uc.restartMigration('userId', 'schoolId'); + const result: UserLoginMigrationDO = await uc.restartMigration(user.id, user.school.id); - expect(result).toEqual(runningMigration); + expect(result).toEqual(migrationAfterRestart); }); }); describe('when the user does not have enough permission', () => { const setup = () => { const user: User = userFactory.buildWithId(); + const migrationBeforeRestart: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ + closedAt: new Date(2023, 5), + }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const error = new ForbiddenException(); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migrationBeforeRestart); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); authorizationService.checkPermission.mockImplementationOnce(() => { - throw new ForbiddenException(); + throw error; }); + + return { + user, + error, + }; }; it('should throw an exception', async () => { - setup(); + const { user, error } = setup(); - const func = async () => uc.restartMigration('userId', 'schoolId'); - - await expect(func).rejects.toThrow(ForbiddenException); + await expect(uc.restartMigration(user.id, user.school.id)).rejects.toThrow(error); }); }); @@ -179,47 +149,19 @@ describe('RestartUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(null); - }; - it('should throw a UserLoginMigrationNotFoundLoggableException', async () => { - setup(); - - const func = async () => uc.restartMigration('userId', 'schoolId'); - - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); - }); - }); - - describe('when the grace period for restarting a migration has expired', () => { - const setup = () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(2023, 6)); - - const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - closedAt: new Date(2023, 5), - finishedAt: new Date(2023, 5), - }); - - const user: User = userFactory.buildWithId(); - - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); + return { + user, + }; }; - it('should throw a UserLoginMigrationGracePeriodExpiredLoggableException', async () => { - setup(); - - const func = async () => uc.restartMigration('userId', 'schoolId'); + it('should throw a UserLoginMigrationNotFoundLoggableException', async () => { + const { user } = setup(); - await expect(func).rejects.toThrow(UserLoginMigrationGracePeriodExpiredLoggableException); + await expect(uc.restartMigration(user.id, user.school.id)).rejects.toThrow( + UserLoginMigrationNotFoundLoggableException + ); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts index 997276c3661..3fcecce5196 100644 --- a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts @@ -1,54 +1,45 @@ +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, -} from '../error'; -import { UserLoginMigrationStartLoggable } from '../loggable'; -import { UserLoginMigrationService } from '../service'; +import { UserLoginMigrationNotFoundLoggableException, UserLoginMigrationStartLoggable } from '../loggable'; +import { SchoolMigrationService, UserLoginMigrationService } from '../service'; @Injectable() export class RestartUserLoginMigrationUc { constructor( private readonly userLoginMigrationService: UserLoginMigrationService, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService, + private readonly schoolMigrationService: SchoolMigrationService, private readonly logger: Logger ) { this.logger.setContext(RestartUserLoginMigrationUc.name); } - async restartMigration(userId: string, schoolId: string): Promise { - await this.checkPermission(userId, schoolId); - - let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( + public async restartMigration(userId: string, schoolId: string): Promise { + const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( schoolId ); if (!userLoginMigration) { throw new UserLoginMigrationNotFoundLoggableException(schoolId); - } else if (userLoginMigration.finishedAt && Date.now() >= userLoginMigration.finishedAt.getTime()) { - throw new UserLoginMigrationGracePeriodExpiredLoggableException( - userLoginMigration.id as string, - userLoginMigration.finishedAt - ); - } else if (userLoginMigration.closedAt) { - userLoginMigration = await this.userLoginMigrationService.restartMigration(schoolId); - - this.logger.info(new UserLoginMigrationStartLoggable(userId, schoolId)); } - return userLoginMigration; - } - - async checkPermission(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + this.authorizationService.checkPermission( + user, + userLoginMigration, + AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) + ); + + const updatedUserLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationService.restartMigration( + userLoginMigration + ); + + await this.schoolMigrationService.unmarkOutdatedUsers(updatedUserLoginMigration); + + this.logger.info(new UserLoginMigrationStartLoggable(userId, updatedUserLoginMigration.id as string)); - const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]); - this.authorizationService.checkPermission(user, school, context); + return updatedUserLoginMigration; } } diff --git a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts index 3f0c03c07ff..b6d3c0210f2 100644 --- a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts @@ -1,16 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../error'; +import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../loggable'; import { UserLoginMigrationService } from '../service'; import { StartUserLoginMigrationUc } from './start-user-login-migration.uc'; -describe('StartUserLoginMigrationUc', () => { +describe(StartUserLoginMigrationUc.name, () => { let module: TestingModule; let uc: StartUserLoginMigrationUc; diff --git a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts index 3dd84cc6ef5..0f7c615bfbd 100644 --- a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts @@ -1,10 +1,13 @@ -import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { Logger } from '@src/core/logger'; import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; -import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../error'; -import { UserLoginMigrationStartLoggable } from '../loggable'; +import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { Logger } from '@src/core/logger'; +import { + SchoolNumberMissingLoggableException, + UserLoginMigrationAlreadyClosedLoggableException, + UserLoginMigrationStartLoggable, +} from '../loggable'; import { UserLoginMigrationService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts index b0ff1f67e54..75fffd4a3c2 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts @@ -1,20 +1,20 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, -} from '../error'; +} from '../loggable'; import { UserLoginMigrationService } from '../service'; import { ToggleUserLoginMigrationUc } from './toggle-user-login-migration.uc'; -describe('ToggleUserLoginMigrationUc', () => { +describe(ToggleUserLoginMigrationUc.name, () => { let module: TestingModule; let uc: ToggleUserLoginMigrationUc; diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts index 45de7b6e1e3..bb020379b9c 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts @@ -1,14 +1,14 @@ -import { Injectable } from '@nestjs/common'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { Logger } from '@src/core/logger'; import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { Injectable } from '@nestjs/common'; +import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { Logger } from '@src/core/logger'; import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, + UserLoginMigrationMandatoryLoggable, UserLoginMigrationNotFoundLoggableException, -} from '../error'; -import { UserLoginMigrationMandatoryLoggable } from '../loggable'; +} from '../loggable'; import { UserLoginMigrationService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts index f5f710ae990..bebd6b7115a 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts @@ -1,8 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { OAuthTokenDto } from '@modules/oauth'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { ProvisioningService } from '@modules/provisioning'; +import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { Page, Permission, LegacySchoolDo, SystemEntity, User, UserLoginMigrationDO } from '@shared/domain'; +import { LegacySchoolDo, Page, Permission, SystemEntity, User, UserLoginMigrationDO } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { legacySchoolDoFactory, @@ -11,22 +19,13 @@ import { userFactory, userLoginMigrationDOFactory, } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; -import { Action, AuthorizationService } from '@modules/authorization'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; -import { ProvisioningService } from '@modules/provisioning'; -import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { Oauth2MigrationParams } from '../controller/dto/oauth2-migration.params'; -import { OAuthMigrationError, SchoolMigrationError, UserLoginMigrationError } from '../error'; -import { PageTypes } from '../interface/page-types.enum'; +import { Logger } from '@src/core/logger'; +import { ExternalSchoolNumberMissingLoggableException } from '../loggable'; +import { InvalidUserLoginMigrationLoggableException } from '../loggable/invalid-user-login-migration.loggable-exception'; import { SchoolMigrationService, UserLoginMigrationService, UserMigrationService } from '../service'; -import { MigrationDto, PageContentDto } from '../service/dto'; import { UserLoginMigrationUc } from './user-login-migration.uc'; -describe('UserLoginMigrationUc', () => { +describe(UserLoginMigrationUc.name, () => { let module: TestingModule; let uc: UserLoginMigrationUc; @@ -37,7 +36,6 @@ describe('UserLoginMigrationUc', () => { let userMigrationService: DeepMocked; let authenticationService: DeepMocked; let authorizationService: DeepMocked; - let logger: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -78,8 +76,8 @@ describe('UserLoginMigrationUc', () => { useValue: createMock(), }, { - provide: LegacyLogger, - useValue: createMock(), + provide: Logger, + useValue: createMock(), }, ], }).compile(); @@ -93,7 +91,6 @@ describe('UserLoginMigrationUc', () => { userMigrationService = module.get(UserMigrationService); authenticationService = module.get(AuthenticationService); authorizationService = module.get(AuthorizationService); - logger = module.get(LegacyLogger); }); afterAll(async () => { @@ -104,34 +101,6 @@ describe('UserLoginMigrationUc', () => { jest.clearAllMocks(); }); - describe('getPageContent is called', () => { - describe('when it should get page-content', () => { - const setup = () => { - const dto: PageContentDto = { - proceedButtonUrl: 'proceed', - cancelButtonUrl: 'cancel', - }; - - userMigrationService.getPageContent.mockResolvedValue(dto); - - return { dto }; - }; - - it('should return a response', async () => { - const { dto } = setup(); - - const testResp: PageContentDto = await uc.getPageContent( - PageTypes.START_FROM_TARGET_SYSTEM, - 'source', - 'target' - ); - - expect(testResp.proceedButtonUrl).toEqual(dto.proceedButtonUrl); - expect(testResp.cancelButtonUrl).toEqual(dto.cancelButtonUrl); - }); - }); - }); - describe('getMigrations', () => { describe('when searching for a users migration', () => { const setup = () => { @@ -143,7 +112,7 @@ describe('UserLoginMigrationUc', () => { startedAt: new Date(), }); - userLoginMigrationService.findMigrationByUser.mockResolvedValue(migrations); + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(migrations); return { userId, migrations }; }; @@ -164,7 +133,7 @@ describe('UserLoginMigrationUc', () => { const setup = () => { const userId = 'userId'; - userLoginMigrationService.findMigrationByUser.mockResolvedValue(null); + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(null); return { userId }; }; @@ -212,8 +181,8 @@ describe('UserLoginMigrationUc', () => { }); const user: User = userFactory.buildWithId(); - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(migration); - authorizationService.getUserWithPermissions.mockResolvedValue(user); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); return { user, schoolId, migration }; }; @@ -244,8 +213,8 @@ describe('UserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); - authorizationService.getUserWithPermissions.mockResolvedValue(user); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(null); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); return { user, schoolId }; }; @@ -273,8 +242,8 @@ describe('UserLoginMigrationUc', () => { const error = new Error('Authorization failed'); - userLoginMigrationService.findMigrationBySchool.mockResolvedValue(migration); - authorizationService.getUserWithPermissions.mockResolvedValue(user); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); authorizationService.checkPermission.mockImplementation(() => { throw error; }); @@ -293,41 +262,16 @@ describe('UserLoginMigrationUc', () => { }); describe('migrate', () => { - describe('when user migrates the from one to another system', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; - - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); - - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ - systems: [sourceSystem.id], - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'oldSchoolExternalId', - }); - const externalUserId = 'externalUserId'; - + describe('when user migrates from one to another system', () => { + const setup = () => { const oauthData: OauthDataDto = new OauthDataDto({ system: new ProvisioningSystemDto({ systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), externalUser: new ExternalUserDto({ - externalId: externalUserId, + externalId: 'externalUserId', }), - externalSchool: new ExternalSchoolDto({ - externalId: 'externalId', - officialSchoolNumber: 'officialSchoolNumber', - name: 'schoolName', - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/succeed', }); const tokenDto: OAuthTokenDto = new OAuthTokenDto({ @@ -336,85 +280,62 @@ describe('UserLoginMigrationUc', () => { accessToken: 'accessToken', }); - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockResolvedValue(schoolDO); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - const message1 = `MIGRATION (userId: currentUserId): Migrates to targetSystem with id ${oauthData.system.systemId}`; - - const message2 = `MIGRATION (userId: currentUserId): Provisioning data received from targetSystem (${ - oauthData.system.systemId ?? 'N/A' - } with data: - { - "officialSchoolNumber": ${oauthData.externalSchool?.officialSchoolNumber ?? 'N/A'}, - "externalSchoolId": ${oauthData.externalSchool?.externalId ?? ''} - "externalUserId": ${oauthData.externalUser.externalId}, - })`; + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'systemId', + closedAt: undefined, + finishedAt: undefined, + }); - const message3 = `MIGRATION (userId: currentUserId): Found school with officialSchoolNumber (${ - oauthData.externalSchool?.officialSchoolNumber ?? '' - })`; + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); + oAuthService.authenticateUser.mockResolvedValueOnce(tokenDto); + provisioningService.getData.mockResolvedValueOnce(oauthData); return { - query, - userMigrationDto, oauthData, tokenDto, - message1, - message2, - message3, }; }; - it('should call authenticate User', async () => { - const { query } = setupMigration(); + it('should authenticate the user with oauth2', async () => { + setup(); - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); - expect(oAuthService.authenticateUser).toHaveBeenCalledWith(query.systemId, query.redirectUri, query.code); + expect(oAuthService.authenticateUser).toHaveBeenCalledWith('systemId', 'redirectUri', 'code'); }); - it('should call get provisioning data', async () => { - const { query, tokenDto } = setupMigration(); + it('should fetch the provisioning data for the user', async () => { + const { tokenDto } = setup(); - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); - expect(provisioningService.getData).toHaveBeenCalledWith( - query.systemId, - tokenDto.idToken, - tokenDto.accessToken - ); + expect(provisioningService.getData).toHaveBeenCalledWith('systemId', tokenDto.idToken, tokenDto.accessToken); }); - it('should call migrate user successfully', async () => { - const { query, oauthData } = setupMigration(); + it('should migrate the user successfully', async () => { + const { oauthData } = setup(); - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); expect(userMigrationService.migrateUser).toHaveBeenCalledWith( 'currentUserId', oauthData.externalUser.externalId, - query.systemId + 'systemId' ); }); it('should remove the jwt from the whitelist', async () => { - const { query } = setupMigration(); + setup(); - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); expect(authenticationService.removeJwtFromWhitelist).toHaveBeenCalledWith('jwt'); }); }); - describe('when migration of user failed', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; - + describe('when external school and official school number is defined and school has to be migrated', () => { + const setup = () => { const sourceSystem: SystemEntity = systemFactory .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); @@ -425,15 +346,13 @@ describe('UserLoginMigrationUc', () => { externalId: 'oldSchoolExternalId', }); - const externalUserId = 'externalUserId'; - const oauthData: OauthDataDto = new OauthDataDto({ system: new ProvisioningSystemDto({ systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), externalUser: new ExternalUserDto({ - externalId: externalUserId, + externalId: 'externalUserId', }), externalSchool: new ExternalSchoolDto({ externalId: 'externalId', @@ -441,63 +360,64 @@ describe('UserLoginMigrationUc', () => { name: 'schoolName', }), }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/error', - }); - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ idToken: 'idToken', refreshToken: 'refreshToken', accessToken: 'accessToken', }); - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockResolvedValue(schoolDO); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'systemId', + closedAt: undefined, + finishedAt: undefined, + }); + + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); + oAuthService.authenticateUser.mockResolvedValueOnce(tokenDto); + provisioningService.getData.mockResolvedValueOnce(oauthData); + schoolMigrationService.getSchoolForMigration.mockResolvedValueOnce(schoolDO); return { - query, - userMigrationDto, + schoolDO, + oauthData, }; }; - it('should throw UserloginMigrationError', async () => { - const { query } = setupMigration(); + it('should get the school that should be migrated', async () => { + const { oauthData } = setup(); - const func = () => uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); - await expect(func).rejects.toThrow(new UserLoginMigrationError()); + expect(schoolMigrationService.getSchoolForMigration).toHaveBeenCalledWith( + 'currentUserId', + oauthData.externalSchool?.externalId, + oauthData.externalSchool?.officialSchoolNumber + ); }); - }); - describe('when schoolnumbers mismatch', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; + it('should migrate the school', async () => { + const { oauthData, schoolDO } = setup(); - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ - systems: [sourceSystem.id], - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'oldSchoolExternalId', - }); - - const externalUserId = 'externalUserId'; + expect(schoolMigrationService.migrateSchool).toHaveBeenCalledWith( + schoolDO, + oauthData.externalSchool?.externalId, + 'systemId' + ); + }); + }); + describe('when external school and official school number is defined and school is already migrated', () => { + const setup = () => { const oauthData: OauthDataDto = new OauthDataDto({ system: new ProvisioningSystemDto({ systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), externalUser: new ExternalUserDto({ - externalId: externalUserId, + externalId: 'externalUserId', }), externalSchool: new ExternalSchoolDto({ externalId: 'externalId', @@ -506,75 +426,48 @@ describe('UserLoginMigrationUc', () => { }), }); - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/error', - }); - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ idToken: 'idToken', refreshToken: 'refreshToken', accessToken: 'accessToken', }); - const error: OAuthMigrationError = new OAuthMigrationError( - 'Current users school is not the same as school found by official school number from target migration system', - 'ext_official_school_number_mismatch', - schoolDO.officialSchoolNumber, - oauthData.externalSchool?.officialSchoolNumber - ); + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'systemId', + closedAt: undefined, + finishedAt: undefined, + }); - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockRejectedValue(error); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); + oAuthService.authenticateUser.mockResolvedValueOnce(tokenDto); + provisioningService.getData.mockResolvedValueOnce(oauthData); + schoolMigrationService.getSchoolForMigration.mockResolvedValueOnce(null); return { - query, - userMigrationDto, - error, + oauthData, }; }; - it('should throw SchoolMigrationError', async () => { - const { query, error } = setupMigration(); + it('should not migrate the school', async () => { + setup(); - const func = () => uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); - await expect(func).rejects.toThrow( - new SchoolMigrationError({ - sourceSchoolNumber: error.officialSchoolNumberFromSource, - targetSchoolNumber: error.officialSchoolNumberFromTarget, - }) - ); + expect(schoolMigrationService.migrateSchool).not.toHaveBeenCalled(); }); }); - describe('when school is missing', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; - - const externalUserId = 'externalUserId'; - + describe('when external school is not defined', () => { + const setup = () => { const oauthData: OauthDataDto = new OauthDataDto({ system: new ProvisioningSystemDto({ systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), externalUser: new ExternalUserDto({ - externalId: externalUserId, + externalId: 'externalUserId', }), - externalSchool: new ExternalSchoolDto({ - externalId: 'externalId', - officialSchoolNumber: 'officialSchoolNumber', - name: 'schoolName', - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/migration/error', }); const tokenDto: OAuthTokenDto = new OAuthTokenDto({ @@ -583,250 +476,129 @@ describe('UserLoginMigrationUc', () => { accessToken: 'accessToken', }); - const error: OAuthMigrationError = new OAuthMigrationError( - 'Official school number from target migration system is missing', - 'ext_official_school_number_missing' - ); - - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockRejectedValue(error); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'systemId', + closedAt: undefined, + finishedAt: undefined, + }); - return { - query, - userMigrationDto, - }; + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); + oAuthService.authenticateUser.mockResolvedValueOnce(tokenDto); + provisioningService.getData.mockResolvedValueOnce(oauthData); + schoolMigrationService.getSchoolForMigration.mockResolvedValueOnce(null); }; - it('should throw SchoolMigrationError', async () => { - const { query } = setupMigration(); + it('should try to migrate the school', async () => { + setup(); - const func = () => uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + await uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri'); - await expect(func).rejects.toThrow(new SchoolMigrationError()); + expect(schoolMigrationService.getSchoolForMigration).not.toHaveBeenCalled(); + expect(schoolMigrationService.migrateSchool).not.toHaveBeenCalled(); }); }); - describe('when external school and official school number is defined and school has to be migrated', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; - - const sourceSystem: SystemEntity = systemFactory - .withOauthConfig() - .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); - - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ - systems: [sourceSystem.id], - officialSchoolNumber: 'officialSchoolNumber', - externalId: 'oldSchoolExternalId', - }); - - const externalUserId = 'externalUserId'; - + describe('when a external school is defined, but has no official school number', () => { + const setup = () => { const oauthData: OauthDataDto = new OauthDataDto({ system: new ProvisioningSystemDto({ systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), externalUser: new ExternalUserDto({ - externalId: externalUserId, + externalId: 'externalUserId', }), externalSchool: new ExternalSchoolDto({ externalId: 'externalId', - officialSchoolNumber: 'officialSchoolNumber', name: 'schoolName', }), }); - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/dashboard', - }); - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ idToken: 'idToken', refreshToken: 'refreshToken', accessToken: 'accessToken', }); - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockResolvedValue(schoolDO); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - const text = `Successfully migrated school (${schoolDO.name} - (${schoolDO.id ?? 'N/A'}) to targetSystem ${ - query.systemId ?? 'N/A' - } which has the externalSchoolId ${oauthData.externalSchool?.externalId ?? 'N/A'}`; + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'systemId', + closedAt: undefined, + finishedAt: undefined, + }); - const message = `MIGRATION (userId: currentUserId): ${text ?? ''}`; + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); + oAuthService.authenticateUser.mockResolvedValueOnce(tokenDto); + provisioningService.getData.mockResolvedValueOnce(oauthData); + schoolMigrationService.getSchoolForMigration.mockResolvedValueOnce(null); return { - query, - userMigrationDto, - schoolDO, oauthData, - message, }; }; - it('should call schoolToMigrate', async () => { - const { oauthData, query } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + it('should throw an error', async () => { + const { oauthData } = setup(); - expect(schoolMigrationService.schoolToMigrate).toHaveBeenCalledWith( - 'currentUserId', - oauthData.externalSchool?.externalId, - oauthData.externalSchool?.officialSchoolNumber + await expect(uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri')).rejects.toThrow( + new ExternalSchoolNumberMissingLoggableException(oauthData.externalSchool?.externalId as string) ); }); + }); - it('should call migrateSchool', async () => { - const { oauthData, query, schoolDO } = setupMigration(); + describe('when no user login migration is running', () => { + const setup = () => { + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(null); + }; - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + it('should throw an error', async () => { + setup(); - expect(schoolMigrationService.migrateSchool).toHaveBeenCalledWith( - oauthData.externalSchool?.externalId, - schoolDO, - 'systemId' + await expect(uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri')).rejects.toThrow( + InvalidUserLoginMigrationLoggableException ); }); - - it('should log migration information', async () => { - const { query, message } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); - - expect(logger.debug).toHaveBeenCalledWith(message); - }); }); - describe('when external school and official school number is defined and school is already migrated', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - externalSchool: new ExternalSchoolDto({ - externalId: 'externalId', - officialSchoolNumber: 'officialSchoolNumber', - name: 'schoolName', - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/dashboard', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', + describe('when the user login migration is closed', () => { + const setup = () => { + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'systemId', + closedAt: new Date(), }); - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockResolvedValue(null); - schoolMigrationService.schoolToMigrate.mockResolvedValueOnce(null); - schoolMigrationService.migrateSchool.mockResolvedValue(); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - const message = `MIGRATION (userId: currentUserId): Found school with officialSchoolNumber (officialSchoolNumber)`; - - return { - query, - userMigrationDto, - oauthData, - message, - }; + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); }; - it('should not call migrateSchool', async () => { - const { query } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); - - expect(schoolMigrationService.migrateSchool).not.toHaveBeenCalled(); - }); - - it('should log migration information', async () => { - const { query, message } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + it('should throw an error', async () => { + setup(); - expect(logger.debug).toHaveBeenCalledWith(message); + await expect(uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri')).rejects.toThrow( + InvalidUserLoginMigrationLoggableException + ); }); }); - describe('when external school is not defined', () => { - const setupMigration = () => { - const query: Oauth2MigrationParams = new Oauth2MigrationParams(); - query.code = 'code'; - query.systemId = 'systemId'; - query.redirectUri = 'redirectUri'; - - const externalUserId = 'externalUserId'; - - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), - }); - - const userMigrationDto: MigrationDto = new MigrationDto({ - redirect: 'https://mock.de/dashboard', - }); - - const tokenDto: OAuthTokenDto = new OAuthTokenDto({ - idToken: 'idToken', - refreshToken: 'refreshToken', - accessToken: 'accessToken', + describe('when trying to migrate to the wrong system', () => { + const setup = () => { + const userLoginMigration = userLoginMigrationDOFactory.build({ + id: new ObjectId().toHexString(), + targetSystemId: 'wrongSystemId', + closedAt: undefined, + finishedAt: undefined, }); - oAuthService.authenticateUser.mockResolvedValue(tokenDto); - provisioningService.getData.mockResolvedValue(oauthData); - schoolMigrationService.schoolToMigrate.mockResolvedValueOnce(null); - userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); - - const message = `Provisioning data received from targetSystem (${oauthData.system.systemId ?? 'N/A'} with data: - { - "officialSchoolNumber": ${oauthData.externalSchool?.officialSchoolNumber ?? 'N/A'}, - "externalSchoolId": ${oauthData.externalSchool?.externalId ?? ''} - "externalUserId": ${oauthData.externalUser.externalId}, - })`; - - return { - query, - userMigrationDto, - message, - }; + userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(userLoginMigration); }; - it('should not call schoolToMigrate', async () => { - const { query } = setupMigration(); - - await uc.migrate('jwt', 'currentUserId', query.systemId, query.code, query.redirectUri); + it('should throw an error', async () => { + setup(); - expect(schoolMigrationService.schoolToMigrate).not.toHaveBeenCalled(); + await expect(uc.migrate('jwt', 'currentUserId', 'systemId', 'code', 'redirectUri')).rejects.toThrow( + InvalidUserLoginMigrationLoggableException + ); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts index a637afe01f6..24442147e5e 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts @@ -1,17 +1,21 @@ -import { ForbiddenException, Injectable } from '@nestjs/common'; -import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { EntityId, Page, Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { LegacyLogger } from '@src/core/logger'; import { AuthenticationService } from '@modules/authentication/services/authentication.service'; import { Action, AuthorizationService } from '@modules/authorization'; import { OAuthTokenDto } from '@modules/oauth'; import { OAuthService } from '@modules/oauth/service/oauth.service'; import { ProvisioningService } from '@modules/provisioning'; import { OauthDataDto } from '@modules/provisioning/dto'; -import { OAuthMigrationError, SchoolMigrationError, UserLoginMigrationError } from '../error'; -import { PageTypes } from '../interface/page-types.enum'; +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { EntityId, LegacySchoolDo, Page, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { Logger } from '@src/core/logger'; +import { + ExternalSchoolNumberMissingLoggableException, + InvalidUserLoginMigrationLoggableException, + SchoolMigrationSuccessfulLoggable, + UserMigrationStartedLoggable, + UserMigrationSuccessfulLoggable, +} from '../loggable'; import { SchoolMigrationService, UserLoginMigrationService, UserMigrationService } from '../service'; -import { MigrationDto, PageContentDto } from '../service/dto'; import { UserLoginMigrationQuery } from './dto'; @Injectable() @@ -24,19 +28,9 @@ export class UserLoginMigrationUc { private readonly schoolMigrationService: SchoolMigrationService, private readonly authenticationService: AuthenticationService, private readonly authorizationService: AuthorizationService, - private readonly logger: LegacyLogger + private readonly logger: Logger ) {} - async getPageContent(pageType: PageTypes, sourceSystem: string, targetSystem: string): Promise { - const content: PageContentDto = await this.userMigrationService.getPageContent( - pageType, - sourceSystem, - targetSystem - ); - - return content; - } - async getMigrations(userId: EntityId, query: UserLoginMigrationQuery): Promise> { let page = new Page([], 0); @@ -77,14 +71,22 @@ export class UserLoginMigrationUc { async migrate( userJwt: string, - currentUserId: string, + currentUserId: EntityId, targetSystemId: EntityId, code: string, redirectUri: string ): Promise { + const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationByUser( + currentUserId + ); + + if (!userLoginMigration || userLoginMigration.closedAt || userLoginMigration.targetSystemId !== targetSystemId) { + throw new InvalidUserLoginMigrationLoggableException(currentUserId, targetSystemId); + } + const tokenDto: OAuthTokenDto = await this.oauthService.authenticateUser(targetSystemId, redirectUri, code); - this.logMigrationInformation(currentUserId, `Migrates to targetSystem with id ${targetSystemId}`); + this.logger.debug(new UserMigrationStartedLoggable(currentUserId, userLoginMigration)); const data: OauthDataDto = await this.provisioningService.getData( targetSystemId, @@ -92,87 +94,32 @@ export class UserLoginMigrationUc { tokenDto.accessToken ); - this.logMigrationInformation(currentUserId, undefined, data, targetSystemId); - if (data.externalSchool) { - let schoolToMigrate: LegacySchoolDo | null; - // TODO: N21-820 after fully switching to the new client login flow, try/catch will be obsolete and schoolToMigrate should throw correct errors - try { - schoolToMigrate = await this.schoolMigrationService.schoolToMigrate( - currentUserId, - data.externalSchool.externalId, - data.externalSchool.officialSchoolNumber - ); - } catch (error: unknown) { - let details: Record | undefined; - - if ( - error instanceof OAuthMigrationError && - error.officialSchoolNumberFromSource && - error.officialSchoolNumberFromTarget - ) { - details = { - sourceSchoolNumber: error.officialSchoolNumberFromSource, - targetSchoolNumber: error.officialSchoolNumberFromTarget, - }; - } - - throw new SchoolMigrationError(details, error); + if (!data.externalSchool.officialSchoolNumber) { + throw new ExternalSchoolNumberMissingLoggableException(data.externalSchool.externalId); } - this.logMigrationInformation( + const schoolToMigrate: LegacySchoolDo | null = await this.schoolMigrationService.getSchoolForMigration( currentUserId, - `Found school with officialSchoolNumber (${data.externalSchool.officialSchoolNumber ?? ''})` + data.externalSchool.externalId, + data.externalSchool.officialSchoolNumber ); if (schoolToMigrate) { await this.schoolMigrationService.migrateSchool( - data.externalSchool.externalId, schoolToMigrate, + data.externalSchool.externalId, targetSystemId ); - this.logMigrationInformation(currentUserId, undefined, data, data.system.systemId, schoolToMigrate); + this.logger.debug(new SchoolMigrationSuccessfulLoggable(schoolToMigrate, userLoginMigration)); } } - const migrationDto: MigrationDto = await this.userMigrationService.migrateUser( - currentUserId, - data.externalUser.externalId, - targetSystemId - ); - - // TODO: N21-820 after implementation of new client login flow, redirects will be obsolete and migrate should throw errors directly - if (migrationDto.redirect.includes('migration/error')) { - throw new UserLoginMigrationError({ userId: currentUserId }); - } + await this.userMigrationService.migrateUser(currentUserId, data.externalUser.externalId, targetSystemId); - this.logMigrationInformation(currentUserId, `Successfully migrated user and redirects to ${migrationDto.redirect}`); + this.logger.debug(new UserMigrationSuccessfulLoggable(currentUserId, userLoginMigration)); await this.authenticationService.removeJwtFromWhitelist(userJwt); } - - private logMigrationInformation( - userId: string, - text?: string, - oauthData?: OauthDataDto, - targetSystemId?: string, - school?: LegacySchoolDo - ) { - let message = `MIGRATION (userId: ${userId}): ${text ?? ''}`; - if (!school && oauthData) { - message += `Provisioning data received from targetSystem (${targetSystemId ?? 'N/A'} with data: - { - "officialSchoolNumber": ${oauthData.externalSchool?.officialSchoolNumber ?? 'N/A'}, - "externalSchoolId": ${oauthData.externalSchool?.externalId ?? ''} - "externalUserId": ${oauthData.externalUser.externalId}, - })`; - } - if (school && oauthData) { - message += `Successfully migrated school (${school.name} - (${school.id ?? 'N/A'}) to targetSystem ${ - targetSystemId ?? 'N/A' - } which has the externalSchoolId ${oauthData.externalSchool?.externalId ?? 'N/A'}`; - } - this.logger.debug(message); - } } diff --git a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts index b30a3f40f4d..377689a4f18 100644 --- a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts +++ b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts @@ -1,13 +1,11 @@ -import { Module } from '@nestjs/common'; -import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '@modules/authentication/authentication.module'; import { AuthorizationModule } from '@modules/authorization'; +import { LegacySchoolModule } from '@modules/legacy-school'; import { OauthModule } from '@modules/oauth'; import { ProvisioningModule } from '@modules/provisioning'; -import { LegacySchoolModule } from '@modules/legacy-school'; +import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; import { UserLoginMigrationController } from './controller/user-login-migration.controller'; -import { UserMigrationController } from './controller/user-migration.controller'; -import { PageContentMapper } from './mapper'; import { CloseUserLoginMigrationUc, RestartUserLoginMigrationUc, @@ -33,8 +31,7 @@ import { UserLoginMigrationModule } from './user-login-migration.module'; RestartUserLoginMigrationUc, ToggleUserLoginMigrationUc, CloseUserLoginMigrationUc, - PageContentMapper, ], - controllers: [UserMigrationController, UserLoginMigrationController], + controllers: [UserLoginMigrationController], }) export class UserLoginMigrationApiModule {} From e28eee00f52c5961dc0f1dec8edaa97e0091ad51 Mon Sep 17 00:00:00 2001 From: Igor Richter <93926487+IgorCapCoder@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:34:41 +0100 Subject: [PATCH 19/23] N21-1336 change tool status determination method (#4534) * coompute tool status via paramter validation and feature flag --- .../common/service/common-tool.service.ts | 4 + .../context-external-tool.module.ts | 14 +- .../context-external-tool/service/index.ts | 1 - .../service/tool-reference.service.spec.ts | 14 +- .../service/tool-reference.service.ts | 9 +- .../service/tool-version-service.spec.ts | 196 ++++++++++++++++++ .../service/tool-version-service.ts | 44 ++++ .../uc/context-external-tool.uc.spec.ts | 3 +- .../uc/context-external-tool.uc.ts | 3 +- .../external-tool/external-tool.module.ts | 6 +- ...xternal-tool-version-increment.service.ts} | 2 +- .../external-tool-version.service.spec.ts | 6 +- .../service/external-tool.service.spec.ts | 10 +- .../service/external-tool.service.ts | 4 +- .../tool/external-tool/service/index.ts | 2 +- .../school-external-tool.module.ts | 3 +- ...l-external-tool-validation.service.spec.ts | 51 ++++- ...school-external-tool-validation.service.ts | 11 +- .../school-external-tool.service.spec.ts | 131 +++++++++--- .../service/school-external-tool.service.ts | 24 ++- apps/server/src/modules/tool/tool-config.ts | 4 + .../service/tool-launch.service.spec.ts | 25 ++- .../service/tool-launch.service.ts | 13 +- config/default.schema.json | 5 + 24 files changed, 496 insertions(+), 89 deletions(-) create mode 100644 apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts rename apps/server/src/modules/tool/external-tool/service/{external-tool-version.service.ts => external-tool-version-increment.service.ts} (98%) diff --git a/apps/server/src/modules/tool/common/service/common-tool.service.ts b/apps/server/src/modules/tool/common/service/common-tool.service.ts index 71f643c81ac..1dccc42ab1f 100644 --- a/apps/server/src/modules/tool/common/service/common-tool.service.ts +++ b/apps/server/src/modules/tool/common/service/common-tool.service.ts @@ -5,8 +5,12 @@ import { ContextExternalTool } from '../../context-external-tool/domain'; import { ToolConfigurationStatus } from '../enum'; import { ToolVersion } from '../interface'; +// TODO N21-1337 remove class when tool versioning is removed @Injectable() export class CommonToolService { + /** + * @deprecated use ToolVersionService + */ determineToolConfigurationStatus( externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool, diff --git a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts index 1afd639f1e7..d73e8a25171 100644 --- a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts +++ b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts @@ -3,26 +3,26 @@ import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; -import { - ContextExternalToolAuthorizableService, - ContextExternalToolService, - ContextExternalToolValidationService, - ToolReferenceService, -} from './service'; +import { ContextExternalToolAuthorizableService, ContextExternalToolService, ToolReferenceService } from './service'; +import { ContextExternalToolValidationService } from './service/context-external-tool-validation.service'; +import { ToolConfigModule } from '../tool-config.module'; +import { ToolVersionService } from './service/tool-version-service'; @Module({ - imports: [CommonToolModule, ExternalToolModule, SchoolExternalToolModule, LoggerModule], + imports: [CommonToolModule, ExternalToolModule, SchoolExternalToolModule, LoggerModule, ToolConfigModule], providers: [ ContextExternalToolService, ContextExternalToolValidationService, ContextExternalToolAuthorizableService, ToolReferenceService, + ToolVersionService, ], exports: [ ContextExternalToolService, ContextExternalToolValidationService, ContextExternalToolAuthorizableService, ToolReferenceService, + ToolVersionService, ], }) export class ContextExternalToolModule {} diff --git a/apps/server/src/modules/tool/context-external-tool/service/index.ts b/apps/server/src/modules/tool/context-external-tool/service/index.ts index 31fedbe42af..30458b4b9c3 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/index.ts @@ -1,4 +1,3 @@ export * from './context-external-tool.service'; -export * from './context-external-tool-validation.service'; export * from './context-external-tool-authorizable.service'; export * from './tool-reference.service'; diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts index e434ab49527..1796b2f681d 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts @@ -3,12 +3,12 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; import { ToolConfigurationStatus } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ToolReference } from '../domain'; import { ContextExternalToolService } from './context-external-tool.service'; import { ToolReferenceService } from './tool-reference.service'; +import { ToolVersionService } from './tool-version-service'; describe('ToolReferenceService', () => { let module: TestingModule; @@ -17,7 +17,7 @@ describe('ToolReferenceService', () => { let externalToolService: DeepMocked; let schoolExternalToolService: DeepMocked; let contextExternalToolService: DeepMocked; - let commonToolService: DeepMocked; + let toolVersionService: DeepMocked; let externalToolLogoService: DeepMocked; beforeAll(async () => { @@ -37,8 +37,8 @@ describe('ToolReferenceService', () => { useValue: createMock(), }, { - provide: CommonToolService, - useValue: createMock(), + provide: ToolVersionService, + useValue: createMock(), }, { provide: ExternalToolLogoService, @@ -51,7 +51,7 @@ describe('ToolReferenceService', () => { externalToolService = module.get(ExternalToolService); schoolExternalToolService = module.get(SchoolExternalToolService); contextExternalToolService = module.get(ContextExternalToolService); - commonToolService = module.get(CommonToolService); + toolVersionService = module.get(ToolVersionService); externalToolLogoService = module.get(ExternalToolLogoService); }); @@ -79,7 +79,7 @@ describe('ToolReferenceService', () => { contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); externalToolService.findById.mockResolvedValueOnce(externalTool); - commonToolService.determineToolConfigurationStatus.mockReturnValue(ToolConfigurationStatus.OUTDATED); + toolVersionService.determineToolConfigurationStatus.mockResolvedValue(ToolConfigurationStatus.OUTDATED); externalToolLogoService.buildLogoUrl.mockReturnValue(logoUrl); return { @@ -96,7 +96,7 @@ describe('ToolReferenceService', () => { await service.getToolReference(contextExternalToolId); - expect(commonToolService.determineToolConfigurationStatus).toHaveBeenCalledWith( + expect(toolVersionService.determineToolConfigurationStatus).toHaveBeenCalledWith( externalTool, schoolExternalTool, contextExternalTool diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts index 02c6a08677e..1f677e3e159 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { ToolConfigurationStatus } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; @@ -9,6 +7,7 @@ import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ContextExternalTool, ToolReference } from '../domain'; import { ToolReferenceMapper } from '../mapper'; import { ContextExternalToolService } from './context-external-tool.service'; +import { ToolVersionService } from './tool-version-service'; @Injectable() export class ToolReferenceService { @@ -16,8 +15,8 @@ export class ToolReferenceService { private readonly externalToolService: ExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, private readonly contextExternalToolService: ContextExternalToolService, - private readonly commonToolService: CommonToolService, - private readonly externalToolLogoService: ExternalToolLogoService + private readonly externalToolLogoService: ExternalToolLogoService, + private readonly toolVersionService: ToolVersionService ) {} async getToolReference(contextExternalToolId: EntityId): Promise { @@ -29,7 +28,7 @@ export class ToolReferenceService { ); const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); - const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( + const status = await this.toolVersionService.determineToolConfigurationStatus( externalTool, schoolExternalTool, contextExternalTool diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts new file mode 100644 index 00000000000..33c77ecb7ea --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts @@ -0,0 +1,196 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ApiValidationError } from '@shared/common'; +import { SchoolExternalToolValidationService } from '../../school-external-tool/service'; +import { ToolVersionService } from './tool-version-service'; +import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; +import { CommonToolService } from '../../common/service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfigurationStatus } from '../../common/enum'; + +describe('ToolVersionService', () => { + let module: TestingModule; + let service: ToolVersionService; + + let contextExternalToolValidationService: DeepMocked; + let schoolExternalToolValidationService: DeepMocked; + let commonToolService: DeepMocked; + let toolFeatures: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ToolVersionService, + { + provide: ContextExternalToolValidationService, + useValue: createMock(), + }, + { + provide: SchoolExternalToolValidationService, + useValue: createMock(), + }, + { + provide: CommonToolService, + useValue: createMock(), + }, + { + provide: ToolFeatures, + useValue: { + toolStatusWithoutVersions: false, + }, + }, + ], + }).compile(); + + service = module.get(ToolVersionService); + contextExternalToolValidationService = module.get(ContextExternalToolValidationService); + schoolExternalToolValidationService = module.get(SchoolExternalToolValidationService); + commonToolService = module.get(CommonToolService); + toolFeatures = module.get(ToolFeatures); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('determineToolConfigurationStatus', () => { + describe('when FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED is false', () => { + const setup = () => { + const externalTool = externalToolFactory.buildWithId(); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(); + + toolFeatures.toolStatusWithoutVersions = false; + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should call CommonToolService', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + + expect(commonToolService.determineToolConfigurationStatus).toHaveBeenCalledWith( + externalTool, + schoolExternalTool, + contextExternalTool + ); + }); + }); + + describe('when FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED is true and validation runs through', () => { + const setup = () => { + const externalTool = externalToolFactory.buildWithId(); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(); + + toolFeatures.toolStatusWithoutVersions = true; + + schoolExternalToolValidationService.validate.mockResolvedValue(); + contextExternalToolValidationService.validate.mockResolvedValueOnce(); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return latest tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + const status: ToolConfigurationStatus = await service.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + expect(status).toEqual(ToolConfigurationStatus.LATEST); + }); + + it('should call schoolExternalToolValidationService', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + + expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); + }); + + it('should call contextExternalToolValidationService', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + + expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); + }); + }); + + describe('when FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED is true and validation throws an error', () => { + const setup = () => { + const externalTool = externalToolFactory.buildWithId(); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(); + + toolFeatures.toolStatusWithoutVersions = true; + + schoolExternalToolValidationService.validate.mockResolvedValue(); + contextExternalToolValidationService.validate.mockRejectedValueOnce(ApiValidationError); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return outdated tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + const status: ToolConfigurationStatus = await service.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + expect(status).toEqual(ToolConfigurationStatus.OUTDATED); + }); + + it('should call schoolExternalToolValidationService', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + + expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); + }); + + it('should call contextExternalToolValidationService', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + + expect(contextExternalToolValidationService.validate).toHaveBeenCalledWith(contextExternalTool); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts new file mode 100644 index 00000000000..2ba0709372a --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { Inject } from '@nestjs/common'; +import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; +import { SchoolExternalToolValidationService } from '../../school-external-tool/service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ExternalTool } from '../../external-tool/domain'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { ContextExternalTool } from '../domain'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; + +@Injectable() +export class ToolVersionService { + constructor( + private readonly contextExternalToolValidationService: ContextExternalToolValidationService, + private readonly schoolExternalToolValidationService: SchoolExternalToolValidationService, + private readonly commonToolService: CommonToolService, + @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures + ) {} + + async determineToolConfigurationStatus( + externalTool: ExternalTool, + schoolExternalTool: SchoolExternalTool, + contextExternalTool: ContextExternalTool + ): Promise { + // TODO N21-1337 remove if statement, when feature flag is removed + if (this.toolFeatures.toolStatusWithoutVersions) { + try { + await this.schoolExternalToolValidationService.validate(schoolExternalTool); + await this.contextExternalToolValidationService.validate(contextExternalTool); + return ToolConfigurationStatus.LATEST; + } catch (err) { + return ToolConfigurationStatus.OUTDATED; + } + } + const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + return status; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts index 0fcca404049..9239e4db2a9 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts @@ -16,7 +16,8 @@ import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ContextExternalTool } from '../domain'; -import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; +import { ContextExternalToolService } from '../service'; +import { ContextExternalToolValidationService } from '../service/context-external-tool-validation.service'; import { ContextExternalToolUc } from './context-external-tool.uc'; describe('ContextExternalToolUc', () => { diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts index 587ecb01c64..80a85321933 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts @@ -12,7 +12,8 @@ import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ContextExternalTool, ContextRef } from '../domain'; -import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; +import { ContextExternalToolService } from '../service'; +import { ContextExternalToolValidationService } from '../service/context-external-tool-validation.service'; import { ContextExternalToolDto } from './dto/context-external-tool.types'; @Injectable() diff --git a/apps/server/src/modules/tool/external-tool/external-tool.module.ts b/apps/server/src/modules/tool/external-tool/external-tool.module.ts index 2fbd2f28edd..9aa0a11e448 100644 --- a/apps/server/src/modules/tool/external-tool/external-tool.module.ts +++ b/apps/server/src/modules/tool/external-tool/external-tool.module.ts @@ -12,7 +12,7 @@ import { ExternalToolService, ExternalToolServiceMapper, ExternalToolValidationService, - ExternalToolVersionService, + ExternalToolVersionIncrementService, } from './service'; import { CommonToolModule } from '../common'; @@ -23,7 +23,7 @@ import { CommonToolModule } from '../common'; ExternalToolServiceMapper, ExternalToolParameterValidationService, ExternalToolValidationService, - ExternalToolVersionService, + ExternalToolVersionIncrementService, ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolRepo, @@ -31,7 +31,7 @@ import { CommonToolModule } from '../common'; exports: [ ExternalToolService, ExternalToolValidationService, - ExternalToolVersionService, + ExternalToolVersionIncrementService, ExternalToolConfigurationService, ExternalToolLogoService, ], diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-version.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-version-increment.service.ts similarity index 98% rename from apps/server/src/modules/tool/external-tool/service/external-tool-version.service.ts rename to apps/server/src/modules/tool/external-tool/service/external-tool-version-increment.service.ts index c71e3b49b91..9e645b97ad2 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-version.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-version-increment.service.ts @@ -3,7 +3,7 @@ import { ExternalTool } from '../domain'; import { CustomParameter } from '../../common/domain'; @Injectable() -export class ExternalToolVersionService { +export class ExternalToolVersionIncrementService { increaseVersionOfNewToolIfNecessary(oldTool: ExternalTool, newTool: ExternalTool): void { if (!oldTool.parameters || !newTool.parameters) { return; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-version.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-version.service.spec.ts index 91566609a8f..dabfd70996e 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-version.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-version.service.spec.ts @@ -1,14 +1,14 @@ import { customParameterFactory, externalToolFactory } from '@shared/testing/factory/domainobject/tool'; -import { ExternalToolVersionService } from './external-tool-version.service'; +import { ExternalToolVersionIncrementService } from './external-tool-version-increment.service'; import { CustomParameterLocation, CustomParameterScope, CustomParameterType } from '../../common/enum'; import { CustomParameter } from '../../common/domain'; import { ExternalTool } from '../domain'; describe('ExternalToolVersionService', () => { - let service: ExternalToolVersionService; + let service: ExternalToolVersionIncrementService; beforeEach(() => { - service = new ExternalToolVersionService(); + service = new ExternalToolVersionIncrementService(); }); const setup = () => { diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts index ddb88ca4ff3..be146f30941 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts @@ -16,7 +16,7 @@ import { ExternalToolSearchQuery } from '../../common/interface'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '../domain'; import { ExternalToolServiceMapper } from './external-tool-service.mapper'; -import { ExternalToolVersionService } from './external-tool-version.service'; +import { ExternalToolVersionIncrementService } from './external-tool-version-increment.service'; import { ExternalToolService } from './external-tool.service'; describe('ExternalToolService', () => { @@ -29,7 +29,7 @@ describe('ExternalToolService', () => { let oauthProviderService: DeepMocked; let mapper: DeepMocked; let encryptionService: DeepMocked; - let versionService: DeepMocked; + let versionService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -64,8 +64,8 @@ describe('ExternalToolService', () => { useValue: createMock(), }, { - provide: ExternalToolVersionService, - useValue: createMock(), + provide: ExternalToolVersionIncrementService, + useValue: createMock(), }, ], }).compile(); @@ -77,7 +77,7 @@ describe('ExternalToolService', () => { oauthProviderService = module.get(OauthProviderService); mapper = module.get(ExternalToolServiceMapper); encryptionService = module.get(DefaultEncryptionService); - versionService = module.get(ExternalToolVersionService); + versionService = module.get(ExternalToolVersionIncrementService); }); afterAll(async () => { diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index 0aa6181682c..d8481c28c71 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -10,7 +10,7 @@ import { ExternalToolSearchQuery } from '../../common/interface'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { ExternalTool, Oauth2ToolConfig } from '../domain'; import { ExternalToolServiceMapper } from './external-tool-service.mapper'; -import { ExternalToolVersionService } from './external-tool-version.service'; +import { ExternalToolVersionIncrementService } from './external-tool-version-increment.service'; @Injectable() export class ExternalToolService { @@ -22,7 +22,7 @@ export class ExternalToolService { private readonly contextExternalToolRepo: ContextExternalToolRepo, @Inject(DefaultEncryptionService) private readonly encryptionService: IEncryptionService, private readonly legacyLogger: LegacyLogger, - private readonly externalToolVersionService: ExternalToolVersionService + private readonly externalToolVersionService: ExternalToolVersionIncrementService ) {} async createExternalTool(externalTool: ExternalTool): Promise { diff --git a/apps/server/src/modules/tool/external-tool/service/index.ts b/apps/server/src/modules/tool/external-tool/service/index.ts index 8cb1df69bc1..f2290ca8969 100644 --- a/apps/server/src/modules/tool/external-tool/service/index.ts +++ b/apps/server/src/modules/tool/external-tool/service/index.ts @@ -1,6 +1,6 @@ export * from './external-tool.service'; export * from './external-tool-service.mapper'; -export * from './external-tool-version.service'; +export * from './external-tool-version-increment.service'; export * from './external-tool-validation.service'; export * from './external-tool-parameter-validation.service'; export * from './external-tool-configuration.service'; diff --git a/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts b/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts index 790f4e716c2..898e159348a 100644 --- a/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts +++ b/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { CommonToolModule } from '../common'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from './service'; import { ExternalToolModule } from '../external-tool'; +import { ToolConfigModule } from '../tool-config.module'; @Module({ - imports: [CommonToolModule, ExternalToolModule], + imports: [CommonToolModule, ExternalToolModule, ToolConfigModule], providers: [SchoolExternalToolService, SchoolExternalToolValidationService], exports: [SchoolExternalToolService, SchoolExternalToolValidationService], }) diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts index e43bdeb42e0..6b4f69d6686 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts @@ -6,6 +6,7 @@ import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; describe('SchoolExternalToolValidationService', () => { let module: TestingModule; @@ -13,6 +14,7 @@ describe('SchoolExternalToolValidationService', () => { let externalToolService: DeepMocked; let commonToolValidationService: DeepMocked; + let toolFeatures: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -26,12 +28,19 @@ describe('SchoolExternalToolValidationService', () => { provide: CommonToolValidationService, useValue: createMock(), }, + { + provide: ToolFeatures, + useValue: { + toolStatusWithoutVersions: false, + }, + }, ], }).compile(); service = module.get(SchoolExternalToolValidationService); externalToolService = module.get(ExternalToolService); commonToolValidationService = module.get(CommonToolValidationService); + toolFeatures = module.get(ToolFeatures); }); afterEach(() => { @@ -51,8 +60,12 @@ describe('SchoolExternalToolValidationService', () => { ...externalToolFactory.buildWithId(), ...externalToolDoMock, }); - externalToolService.findById.mockResolvedValue(externalTool); + const schoolExternalToolId = schoolExternalTool.id as string; + + externalToolService.findById.mockResolvedValue(externalTool); + toolFeatures.toolStatusWithoutVersions = true; + return { schoolExternalTool, ExternalTool, @@ -87,9 +100,45 @@ describe('SchoolExternalToolValidationService', () => { schoolExternalTool ); }); + + it('should not throw error', async () => { + const { schoolExternalTool } = setup({ version: 8383 }, { toolVersion: 1337 }); + + const func = () => service.validate(schoolExternalTool); + + await expect(func()).resolves.not.toThrow(); + }); }); + }); + // TODO N21-1337 refactor after feature flag is removed + describe('validate with FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED on false', () => { describe('when version of externalTool and schoolExternalTool are different', () => { + const setup = ( + externalToolDoMock?: Partial, + schoolExternalToolDoMock?: Partial + ) => { + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + ...schoolExternalToolFactory.buildWithId(), + ...schoolExternalToolDoMock, + }); + const externalTool: ExternalTool = new ExternalTool({ + ...externalToolFactory.buildWithId(), + ...externalToolDoMock, + }); + + const schoolExternalToolId = schoolExternalTool.id as string; + + externalToolService.findById.mockResolvedValue(externalTool); + toolFeatures.toolStatusWithoutVersions = false; + + return { + schoolExternalTool, + ExternalTool, + schoolExternalToolId, + }; + }; + it('should throw error', async () => { const { schoolExternalTool } = setup({ version: 8383 }, { toolVersion: 1337 }); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts index 315d738ca64..3474aa90f5e 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts @@ -1,15 +1,17 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { ValidationError } from '@shared/common'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; @Injectable() export class SchoolExternalToolValidationService { constructor( private readonly externalToolService: ExternalToolService, - private readonly commonToolValidationService: CommonToolValidationService + private readonly commonToolValidationService: CommonToolValidationService, + @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures ) {} async validate(schoolExternalTool: SchoolExternalTool): Promise { @@ -17,8 +19,9 @@ export class SchoolExternalToolValidationService { const loadedExternalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); - this.checkVersionMatch(schoolExternalTool.toolVersion, loadedExternalTool.version); - + if (!this.toolFeatures.toolStatusWithoutVersions) { + this.checkVersionMatch(schoolExternalTool.toolVersion, loadedExternalTool.version); + } this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, schoolExternalTool); } diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts index 52f9b0a4c02..b8c2789322d 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts @@ -3,11 +3,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchoolExternalToolRepo } from '@shared/repo'; import { externalToolFactory } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; import { schoolExternalToolFactory } from '@shared/testing/factory/domainobject/tool/school-external-tool.factory'; +import { ApiValidationError } from '@shared/common'; import { ToolConfigurationStatus } from '../../common/enum'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolService } from './school-external-tool.service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; describe('SchoolExternalToolService', () => { let module: TestingModule; @@ -15,6 +18,8 @@ describe('SchoolExternalToolService', () => { let schoolExternalToolRepo: DeepMocked; let externalToolService: DeepMocked; + let schoolExternalToolValidationService: DeepMocked; + let toolFearures: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -28,19 +33,33 @@ describe('SchoolExternalToolService', () => { provide: ExternalToolService, useValue: createMock(), }, + { + provide: SchoolExternalToolValidationService, + useValue: createMock(), + }, + { + provide: ToolFeatures, + useValue: { + toolStatusWithoutVersions: false, + }, + }, ], }).compile(); service = module.get(SchoolExternalToolService); schoolExternalToolRepo = module.get(SchoolExternalToolRepo); externalToolService = module.get(ExternalToolService); + schoolExternalToolValidationService = module.get(SchoolExternalToolValidationService); + toolFearures = module.get(ToolFeatures); }); - const setup = () => { + // TODO N21-1337 refactor setup into the describe blocks + const legacySetup = () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); const externalTool: ExternalTool = externalToolFactory.buildWithId(); schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); + toolFearures.toolStatusWithoutVersions = false; return { schoolExternalTool, @@ -52,7 +71,7 @@ describe('SchoolExternalToolService', () => { describe('findSchoolExternalTools', () => { describe('when called with query', () => { it('should call repo with query', async () => { - const { schoolExternalTool } = setup(); + const { schoolExternalTool } = legacySetup(); await service.findSchoolExternalTools(schoolExternalTool); @@ -60,7 +79,7 @@ describe('SchoolExternalToolService', () => { }); it('should return schoolExternalTool array', async () => { - const { schoolExternalTool } = setup(); + const { schoolExternalTool } = legacySetup(); schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool, schoolExternalTool]); const result: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -72,7 +91,7 @@ describe('SchoolExternalToolService', () => { describe('enrichDataFromExternalTool', () => { it('should call the externalToolService', async () => { - const { schoolExternalTool } = setup(); + const { schoolExternalTool } = legacySetup(); schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); await service.findSchoolExternalTools(schoolExternalTool); @@ -81,44 +100,102 @@ describe('SchoolExternalToolService', () => { }); describe('when determine status', () => { - describe('when external tool version is greater', () => { - it('should return status outdated', async () => { - const { schoolExternalTool, externalTool } = setup(); - externalTool.version = 1337; - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); + describe('when FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED is false', () => { + describe('when external tool version is greater', () => { + it('should return status outdated', async () => { + const { schoolExternalTool, externalTool } = legacySetup(); + externalTool.version = 1337; + schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); + externalToolService.findById.mockResolvedValue(externalTool); + + const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( + schoolExternalTool + ); + + expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.OUTDATED); + }); + }); - const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); + describe('when external tool version is lower', () => { + it('should return status latest', async () => { + const { schoolExternalTool, externalTool } = legacySetup(); + schoolExternalTool.toolVersion = 1; + externalTool.version = 0; + schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); + externalToolService.findById.mockResolvedValue(externalTool); - expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.OUTDATED); + const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( + schoolExternalTool + ); + + expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.LATEST); + }); + }); + + describe('when external tool version is equal', () => { + it('should return status latest', async () => { + const { schoolExternalTool, externalTool } = legacySetup(); + schoolExternalTool.toolVersion = 1; + externalTool.version = 1; + schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); + externalToolService.findById.mockResolvedValue(externalTool); + + const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( + schoolExternalTool + ); + + expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.LATEST); + }); }); }); - describe('when external tool version is lower', () => { - it('should return status latest', async () => { - const { schoolExternalTool, externalTool } = setup(); - schoolExternalTool.toolVersion = 1; - externalTool.version = 0; + describe('when FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED is true and validation goes through', () => { + const setup = () => { + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const externalTool: ExternalTool = externalToolFactory.buildWithId(); + schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); externalToolService.findById.mockResolvedValue(externalTool); + schoolExternalToolValidationService.validate.mockResolvedValue(); + toolFearures.toolStatusWithoutVersions = true; + + return { + schoolExternalTool, + }; + }; + + it('should return latest tool status', async () => { + const { schoolExternalTool } = setup(); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); + expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.LATEST); }); }); - describe('when external tool version is equal', () => { - it('should return status latest', async () => { - const { schoolExternalTool, externalTool } = setup(); - schoolExternalTool.toolVersion = 1; - externalTool.version = 1; + describe('when FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED is true and validation throws error', () => { + const setup = () => { + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const externalTool: ExternalTool = externalToolFactory.buildWithId(); + schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); externalToolService.findById.mockResolvedValue(externalTool); + schoolExternalToolValidationService.validate.mockRejectedValue(ApiValidationError); + toolFearures.toolStatusWithoutVersions = true; + + return { + schoolExternalTool, + }; + }; + + it('should return outdated tool status', async () => { + const { schoolExternalTool } = setup(); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); - expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.LATEST); + expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); + expect(schoolExternalToolDOs[0].status).toEqual(ToolConfigurationStatus.OUTDATED); }); }); }); @@ -127,7 +204,7 @@ describe('SchoolExternalToolService', () => { describe('deleteSchoolExternalToolById', () => { describe('when schoolExternalToolId is given', () => { it('should call the schoolExternalToolRepo', async () => { - const { schoolExternalToolId } = setup(); + const { schoolExternalToolId } = legacySetup(); await service.deleteSchoolExternalToolById(schoolExternalToolId); @@ -139,7 +216,7 @@ describe('SchoolExternalToolService', () => { describe('findById', () => { describe('when schoolExternalToolId is given', () => { it('should call schoolExternalToolRepo.findById', async () => { - const { schoolExternalToolId } = setup(); + const { schoolExternalToolId } = legacySetup(); await service.findById(schoolExternalToolId); @@ -151,7 +228,7 @@ describe('SchoolExternalToolService', () => { describe('saveSchoolExternalTool', () => { describe('when schoolExternalTool is given', () => { it('should call schoolExternalToolRepo.save', async () => { - const { schoolExternalTool } = setup(); + const { schoolExternalTool } = legacySetup(); await service.saveSchoolExternalTool(schoolExternalTool); @@ -159,7 +236,7 @@ describe('SchoolExternalToolService', () => { }); it('should enrich data from externalTool', async () => { - const { schoolExternalTool } = setup(); + const { schoolExternalTool } = legacySetup(); await service.saveSchoolExternalTool(schoolExternalTool); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts index 2f011560f6a..e654f82f96a 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { SchoolExternalToolRepo } from '@shared/repo'; import { ToolConfigurationStatus } from '../../common/enum'; @@ -6,12 +6,16 @@ import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; +import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; @Injectable() export class SchoolExternalToolService { constructor( private readonly schoolExternalToolRepo: SchoolExternalToolRepo, - private readonly externalToolService: ExternalToolService + private readonly externalToolService: ExternalToolService, + private readonly schoolExternalToolValidationService: SchoolExternalToolValidationService, + @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures ) {} async findById(schoolExternalToolId: EntityId): Promise { @@ -39,7 +43,7 @@ export class SchoolExternalToolService { private async enrichDataFromExternalTool(tool: SchoolExternalTool): Promise { const externalTool: ExternalTool = await this.externalToolService.findById(tool.toolId); - const status: ToolConfigurationStatus = this.determineStatus(tool, externalTool); + const status: ToolConfigurationStatus = await this.determineStatus(tool, externalTool); const schoolExternalTool: SchoolExternalTool = new SchoolExternalTool({ ...tool, status, @@ -49,7 +53,19 @@ export class SchoolExternalToolService { return schoolExternalTool; } - private determineStatus(tool: SchoolExternalTool, externalTool: ExternalTool): ToolConfigurationStatus { + private async determineStatus( + tool: SchoolExternalTool, + externalTool: ExternalTool + ): Promise { + if (this.toolFeatures.toolStatusWithoutVersions) { + try { + await this.schoolExternalToolValidationService.validate(tool); + return ToolConfigurationStatus.LATEST; + } catch (err) { + return ToolConfigurationStatus.OUTDATED; + } + } + if (externalTool.version <= tool.toolVersion) { return ToolConfigurationStatus.LATEST; } diff --git a/apps/server/src/modules/tool/tool-config.ts b/apps/server/src/modules/tool/tool-config.ts index 2bd47089286..1405f0c7a1d 100644 --- a/apps/server/src/modules/tool/tool-config.ts +++ b/apps/server/src/modules/tool/tool-config.ts @@ -6,6 +6,8 @@ export interface IToolFeatures { ctlToolsTabEnabled: boolean; ltiToolsTabEnabled: boolean; contextConfigurationEnabled: boolean; + // TODO N21-1337 refactor after feature flag is removed + toolStatusWithoutVersions: boolean; maxExternalToolLogoSizeInBytes: number; backEndUrl: string; } @@ -15,6 +17,8 @@ export default class ToolConfiguration { ctlToolsTabEnabled: Configuration.get('FEATURE_CTL_TOOLS_TAB_ENABLED') as boolean, ltiToolsTabEnabled: Configuration.get('FEATURE_LTI_TOOLS_TAB_ENABLED') as boolean, contextConfigurationEnabled: Configuration.get('FEATURE_CTL_CONTEXT_CONFIGURATION_ENABLED') as boolean, + // TODO N21-1337 refactor after feature flag is removed + toolStatusWithoutVersions: Configuration.get('FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED') as boolean, maxExternalToolLogoSizeInBytes: Configuration.get('CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES') as number, backEndUrl: Configuration.get('PUBLIC_BACKEND_URL') as string, }; diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts index e4f9eaa6113..16e8e093e9e 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts @@ -8,7 +8,6 @@ import { schoolExternalToolFactory, } from '@shared/testing'; import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { BasicToolConfig, ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; @@ -23,6 +22,7 @@ import { OAuth2ToolLaunchStrategy, } from './launch-strategy'; import { ToolLaunchService } from './tool-launch.service'; +import { ToolVersionService } from '../../context-external-tool/service/tool-version-service'; describe('ToolLaunchService', () => { let module: TestingModule; @@ -31,7 +31,7 @@ describe('ToolLaunchService', () => { let schoolExternalToolService: DeepMocked; let externalToolService: DeepMocked; let basicToolLaunchStrategy: DeepMocked; - let commonToolService: DeepMocked; + let toolVersionService: DeepMocked; beforeEach(async () => { module = await Test.createTestingModule({ @@ -58,8 +58,8 @@ describe('ToolLaunchService', () => { useValue: createMock(), }, { - provide: CommonToolService, - useValue: createMock(), + provide: ToolVersionService, + useValue: createMock(), }, ], }).compile(); @@ -68,7 +68,7 @@ describe('ToolLaunchService', () => { schoolExternalToolService = module.get(SchoolExternalToolService); externalToolService = module.get(ExternalToolService); basicToolLaunchStrategy = module.get(BasicToolLaunchStrategy); - commonToolService = module.get(CommonToolService); + toolVersionService = module.get(ToolVersionService); }); afterAll(async () => { @@ -107,7 +107,7 @@ describe('ToolLaunchService', () => { schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); - commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); + toolVersionService.determineToolConfigurationStatus.mockResolvedValueOnce(ToolConfigurationStatus.LATEST); return { launchDataDO, @@ -146,6 +146,14 @@ describe('ToolLaunchService', () => { expect(externalToolService.findById).toHaveBeenCalledWith(launchParams.schoolExternalTool.toolId); }); + + it('should call toolVersionService', async () => { + const { launchParams } = setup(); + + await service.getLaunchData('userId', launchParams.contextExternalTool); + + expect(toolVersionService.determineToolConfigurationStatus).toHaveBeenCalled(); + }); }); describe('when the tool config type is unknown', () => { @@ -165,7 +173,7 @@ describe('ToolLaunchService', () => { schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); - commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); + toolVersionService.determineToolConfigurationStatus.mockResolvedValueOnce(ToolConfigurationStatus.LATEST); return { launchParams, @@ -210,10 +218,9 @@ describe('ToolLaunchService', () => { schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); - commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.OUTDATED); + toolVersionService.determineToolConfigurationStatus.mockResolvedValueOnce(ToolConfigurationStatus.OUTDATED); return { - launchDataDO, launchParams, userId, contextExternalToolId: contextExternalTool.id as string, diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts index abb5598796f..5b090f9778a 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts @@ -1,7 +1,6 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; @@ -16,6 +15,7 @@ import { Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, } from './launch-strategy'; +import { ToolVersionService } from '../../context-external-tool/service/tool-version-service'; @Injectable() export class ToolLaunchService { @@ -27,7 +27,7 @@ export class ToolLaunchService { private readonly basicToolLaunchStrategy: BasicToolLaunchStrategy, private readonly lti11ToolLaunchStrategy: Lti11ToolLaunchStrategy, private readonly oauth2ToolLaunchStrategy: OAuth2ToolLaunchStrategy, - private readonly commonToolService: CommonToolService + private readonly toolVersionService: ToolVersionService ) { this.strategies = new Map(); this.strategies.set(ToolConfigType.BASIC, basicToolLaunchStrategy); @@ -53,7 +53,7 @@ export class ToolLaunchService { const { externalTool, schoolExternalTool } = await this.loadToolHierarchy(schoolExternalToolId); - this.isToolStatusLatestOrThrow(userId, externalTool, schoolExternalTool, contextExternalTool); + await this.isToolStatusLatestOrThrow(userId, externalTool, schoolExternalTool, contextExternalTool); const strategy: IToolLaunchStrategy | undefined = this.strategies.get(externalTool.config.type); @@ -83,17 +83,18 @@ export class ToolLaunchService { }; } - private isToolStatusLatestOrThrow( + private async isToolStatusLatestOrThrow( userId: EntityId, externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool, contextExternalTool: ContextExternalTool - ): void { - const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( + ): Promise { + const status = await this.toolVersionService.determineToolConfigurationStatus( externalTool, schoolExternalTool, contextExternalTool ); + if (status !== ToolConfigurationStatus.LATEST) { throw new ToolStatusOutdatedLoggableException(userId, contextExternalTool.id ?? ''); } diff --git a/config/default.schema.json b/config/default.schema.json index ae43648f3ea..b971968a4ae 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1323,6 +1323,11 @@ "default": false, "description": "Enables groups of type class in courses" }, + "FEATURE_COMPUTE_TOOL_STATUS_WITHOUT_VERSIONS_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enables the calculation of the outdated status of an external tool without the usage of the db attribute version" + }, "TSP_SCHOOL_SYNCER": { "type": "object", "description": "TSP School Syncer properties", From b3b4cee3b13c783c3a67619db5cab92d1b743e81 Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Tue, 14 Nov 2023 16:16:57 +0100 Subject: [PATCH 20/23] N21-1447 user-login-migration refactorings (#4556) --- .../src/modules/authentication/index.ts | 1 + .../modules/authentication/services/index.ts | 2 + .../strategy/oauth2.strategy.spec.ts | 3 +- .../strategy/oauth2.strategy.ts | 3 +- apps/server/src/modules/oauth/index.ts | 1 + .../server/src/modules/oauth/service/index.ts | 3 + .../modules/oauth/service/oauth.service.ts | 3 +- .../modules/oauth/uc/hydra-oauth.uc.spec.ts | 3 +- apps/server/src/modules/provisioning/index.ts | 2 +- ...ation-already-closed.loggable-exception.ts | 2 +- ...login-migration-mandatory.loggable.spec.ts | 33 ++++++ ...user-login-migration-mandatory.loggable.ts | 2 +- ...ser-login-migration-start.loggable.spec.ts | 32 ++++++ .../user-login-migration-start.loggable.ts | 2 +- .../user-login-migration/service/dto/index.ts | 1 - .../service/dto/school-migration-flags.ts | 4 - .../service/migration-check.service.spec.ts | 3 +- .../service/migration-check.service.ts | 45 +++++--- .../service/school-migration.service.spec.ts | 8 +- .../service/school-migration.service.ts | 16 ++- .../user-login-migration.service.spec.ts | 96 ++++++++-------- .../service/user-login-migration.service.ts | 14 ++- .../uc/restart-user-login-migration.uc.ts | 2 +- .../uc/start-user-login-migration.uc.ts | 13 +-- .../uc/toggle-user-login-migration.uc.spec.ts | 103 +++++------------- .../uc/toggle-user-login-migration.uc.ts | 31 ++---- .../uc/user-login-migration.uc.spec.ts | 35 +++--- .../uc/user-login-migration.uc.ts | 8 +- 28 files changed, 248 insertions(+), 223 deletions(-) create mode 100644 apps/server/src/modules/authentication/services/index.ts create mode 100644 apps/server/src/modules/oauth/service/index.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts delete mode 100644 apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts diff --git a/apps/server/src/modules/authentication/index.ts b/apps/server/src/modules/authentication/index.ts index 59e749c7abc..3a3ea0c2755 100644 --- a/apps/server/src/modules/authentication/index.ts +++ b/apps/server/src/modules/authentication/index.ts @@ -1,3 +1,4 @@ export { ICurrentUser } from './interface'; export { JWT, CurrentUser, Authenticate } from './decorator'; export { AuthenticationModule } from './authentication.module'; +export { AuthenticationService } from './services'; diff --git a/apps/server/src/modules/authentication/services/index.ts b/apps/server/src/modules/authentication/services/index.ts new file mode 100644 index 00000000000..45277bbf1c5 --- /dev/null +++ b/apps/server/src/modules/authentication/services/index.ts @@ -0,0 +1,2 @@ +export * from './ldap.service'; +export * from './authentication.service'; diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts index e6bf4ef2fa6..428f3c5e048 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts @@ -1,8 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, RoleName } from '@shared/domain'; diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts index e83e9174abc..e5bc6f942f8 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts @@ -1,7 +1,6 @@ import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { UserDO } from '@shared/domain/domainobject/user.do'; diff --git a/apps/server/src/modules/oauth/index.ts b/apps/server/src/modules/oauth/index.ts index 40724ec25c2..67f5f98b440 100644 --- a/apps/server/src/modules/oauth/index.ts +++ b/apps/server/src/modules/oauth/index.ts @@ -1,2 +1,3 @@ export * from './oauth.module'; export * from './interface'; +export * from './service'; diff --git a/apps/server/src/modules/oauth/service/index.ts b/apps/server/src/modules/oauth/service/index.ts new file mode 100644 index 00000000000..d9123c8e5bd --- /dev/null +++ b/apps/server/src/modules/oauth/service/index.ts @@ -0,0 +1,3 @@ +export * from './hydra.service'; +export * from './oauth.service'; +export * from './oauth-adapter.service'; diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 9a484a52d47..5e787d79082 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,7 +1,6 @@ import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { LegacySchoolService } from '@modules/legacy-school'; -import { ProvisioningService } from '@modules/provisioning'; -import { OauthDataDto } from '@modules/provisioning/dto'; +import { ProvisioningService, OauthDataDto } from '@modules/provisioning'; import { SystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts index 339bc6c09d9..b889995b9e5 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts @@ -7,8 +7,7 @@ import { OauthConfig } from '@shared/domain'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; -import { HydraSsoService } from '@modules/oauth/service/hydra.service'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { OAuthService, HydraSsoService } from '@modules/oauth'; import { AxiosResponse } from 'axios'; import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; diff --git a/apps/server/src/modules/provisioning/index.ts b/apps/server/src/modules/provisioning/index.ts index 0e0bc64d04a..caf7d1f3483 100644 --- a/apps/server/src/modules/provisioning/index.ts +++ b/apps/server/src/modules/provisioning/index.ts @@ -1,4 +1,4 @@ export * from './provisioning.module'; -export * from './dto/provisioning.dto'; +export * from './dto'; export * from './service/provisioning.service'; export * from './strategy'; diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts index 321eba624f9..4e5fec1c31b 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts @@ -3,7 +3,7 @@ import { EntityId } from '@shared/domain'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; export class UserLoginMigrationAlreadyClosedLoggableException extends UnprocessableEntityException implements Loggable { - constructor(private readonly userLoginMigrationId: EntityId, private readonly closedAt: Date) { + constructor(private readonly closedAt: Date, private readonly userLoginMigrationId?: EntityId) { super(); } diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts new file mode 100644 index 00000000000..a2b1cde606b --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts @@ -0,0 +1,33 @@ +import { ObjectId } from 'bson'; +import { UserLoginMigrationMandatoryLoggable } from './user-login-migration-mandatory.loggable'; + +describe(UserLoginMigrationMandatoryLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const userLoginMigrationId = new ObjectId().toHexString(); + const exception = new UserLoginMigrationMandatoryLoggable(userId, userLoginMigrationId, true); + + return { + exception, + userId, + userLoginMigrationId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, userLoginMigrationId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'The school administrator changed the requirement status of the user login migration for his school.', + data: { + userId, + userLoginMigrationId, + mandatory: true, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts index 93312a60402..b3bf5724aaf 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts @@ -4,7 +4,7 @@ import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from export class UserLoginMigrationMandatoryLoggable implements Loggable { constructor( private readonly userId: EntityId, - private readonly userLoginMigrationId: EntityId, + private readonly userLoginMigrationId: EntityId | undefined, private readonly mandatory: boolean ) {} diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts new file mode 100644 index 00000000000..f15ac763e5c --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts @@ -0,0 +1,32 @@ +import { ObjectId } from 'bson'; +import { UserLoginMigrationStartLoggable } from './user-login-migration-start.loggable'; + +describe(UserLoginMigrationStartLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const userLoginMigrationId = new ObjectId().toHexString(); + const exception = new UserLoginMigrationStartLoggable(userId, userLoginMigrationId); + + return { + exception, + userId, + userLoginMigrationId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, userLoginMigrationId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'The school administrator started the migration for his school.', + data: { + userId, + userLoginMigrationId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts index f1ccb50d4d3..150ce3117b1 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts @@ -2,7 +2,7 @@ import { EntityId } from '@shared/domain'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; export class UserLoginMigrationStartLoggable implements Loggable { - constructor(private readonly userId: EntityId, private readonly userLoginMigrationId: EntityId) {} + constructor(private readonly userId: EntityId, private readonly userLoginMigrationId: EntityId | undefined) {} getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { return { diff --git a/apps/server/src/modules/user-login-migration/service/dto/index.ts b/apps/server/src/modules/user-login-migration/service/dto/index.ts index 7a92999ded8..bbc5344f4dd 100644 --- a/apps/server/src/modules/user-login-migration/service/dto/index.ts +++ b/apps/server/src/modules/user-login-migration/service/dto/index.ts @@ -1,3 +1,2 @@ export * from './migration.dto'; export * from './page-content.dto'; -export * from './school-migration-flags'; diff --git a/apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts b/apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts deleted file mode 100644 index b2b0f8b6e52..00000000000 --- a/apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface SchoolMigrationFlags { - isPossible: boolean; - isMandatory: boolean; -} diff --git a/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts b/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts index 7afc017cf98..c3e2d1eac91 100644 --- a/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts @@ -44,7 +44,7 @@ describe('MigrationCheckService', () => { await module.close(); }); - describe('shouldUserMigrate is called', () => { + describe('shouldUserMigrate', () => { describe('when no school with the official school number was found', () => { const setup = () => { schoolService.getSchoolBySchoolNumber.mockResolvedValue(null); @@ -87,6 +87,7 @@ describe('MigrationCheckService', () => { targetSystemId: 'targetSystemId', startedAt: new Date('2023-03-03'), }); + schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); userService.findByExternalId.mockResolvedValue(null); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); diff --git a/apps/server/src/modules/user-login-migration/service/migration-check.service.ts b/apps/server/src/modules/user-login-migration/service/migration-check.service.ts index 70d0ab94066..4c30cc2f8c0 100644 --- a/apps/server/src/modules/user-login-migration/service/migration-check.service.ts +++ b/apps/server/src/modules/user-login-migration/service/migration-check.service.ts @@ -1,8 +1,8 @@ +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { EntityId, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; @Injectable() export class MigrationCheckService { @@ -12,22 +12,39 @@ export class MigrationCheckService { private readonly userLoginMigrationRepo: UserLoginMigrationRepo ) {} - async shouldUserMigrate(externalUserId: string, systemId: EntityId, officialSchoolNumber: string): Promise { + public async shouldUserMigrate( + externalUserId: string, + systemId: EntityId, + officialSchoolNumber: string + ): Promise { const school: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); - if (school && school.id) { - const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId( - school.id - ); + if (!school?.id) { + return false; + } - const user: UserDO | null = await this.userService.findByExternalId(externalUserId, systemId); + const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(school.id); - if (user?.lastLoginSystemChange && userLoginMigration && !userLoginMigration.closedAt) { - const hasMigrated: boolean = user.lastLoginSystemChange > userLoginMigration.startedAt; - return !hasMigrated; - } - return !!userLoginMigration && !userLoginMigration.closedAt; + if (!userLoginMigration || !this.isMigrationActive(userLoginMigration)) { + return false; } - return false; + + const user: UserDO | null = await this.userService.findByExternalId(externalUserId, systemId); + + if (this.isUserMigrated(user, userLoginMigration)) { + return false; + } + + return true; + } + + private isUserMigrated(user: UserDO | null, userLoginMigration: UserLoginMigrationDO): boolean { + return ( + !!user && user.lastLoginSystemChange !== undefined && user.lastLoginSystemChange > userLoginMigration.startedAt + ); + } + + private isMigrationActive(userLoginMigration: UserLoginMigrationDO): boolean { + return !userLoginMigration.closedAt; } } diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts index 988e6de9b01..c84339c9fbf 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts @@ -303,8 +303,8 @@ describe(SchoolMigrationService.name, () => { const closedAt: Date = new Date('2023-05-01'); const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', + schoolId: new ObjectId().toHexString(), + targetSystemId: new ObjectId().toHexString(), startedAt: new Date('2023-05-01'), closedAt, finishedAt: new Date('2023-05-01'), @@ -379,7 +379,7 @@ describe(SchoolMigrationService.name, () => { }; }; - it('should call userLoginMigrationRepo.findBySchoolId', async () => { + it('should find user login migration by school id', async () => { setup(); await service.hasSchoolMigratedUser('schoolId'); @@ -399,7 +399,7 @@ describe(SchoolMigrationService.name, () => { }; }; - it('should call userService.findUsers', async () => { + it('should call user service to find users', async () => { const { userLoginMigration } = setup(); await service.hasSchoolMigratedUser('schoolId'); diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts index aa5173dcff6..41cabc13d03 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts @@ -20,7 +20,11 @@ export class SchoolMigrationService { private readonly userLoginMigrationRepo: UserLoginMigrationRepo ) {} - async migrateSchool(existingSchool: LegacySchoolDo, externalId: string, targetSystemId: string): Promise { + public async migrateSchool( + existingSchool: LegacySchoolDo, + externalId: string, + targetSystemId: string + ): Promise { const schoolDOCopy: LegacySchoolDo = new LegacySchoolDo({ ...existingSchool }); try { @@ -45,7 +49,7 @@ export class SchoolMigrationService { await this.schoolService.save(school); } - private async tryRollbackMigration(originalSchoolDO: LegacySchoolDo) { + private async tryRollbackMigration(originalSchoolDO: LegacySchoolDo): Promise { try { await this.schoolService.save(originalSchoolDO); } catch (error: unknown) { @@ -55,7 +59,7 @@ export class SchoolMigrationService { } } - async getSchoolForMigration( + public async getSchoolForMigration( userId: string, externalId: string, officialSchoolNumber: string @@ -89,7 +93,7 @@ export class SchoolMigrationService { return isExternalIdEquivalent; } - async markUnmigratedUsersAsOutdated(userLoginMigration: UserLoginMigrationDO): Promise { + public async markUnmigratedUsersAsOutdated(userLoginMigration: UserLoginMigrationDO): Promise { const startTime: number = performance.now(); const notMigratedUsers: Page = await this.userService.findUsers({ @@ -112,7 +116,7 @@ export class SchoolMigrationService { ); } - async unmarkOutdatedUsers(userLoginMigration: UserLoginMigrationDO): Promise { + public async unmarkOutdatedUsers(userLoginMigration: UserLoginMigrationDO): Promise { const startTime: number = performance.now(); const migratedUsers: Page = await this.userService.findUsers({ @@ -132,7 +136,7 @@ export class SchoolMigrationService { ); } - async hasSchoolMigratedUser(schoolId: string): Promise { + public async hasSchoolMigratedUser(schoolId: string): Promise { const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); if (!userLoginMigration) { diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts index ac7f3bc4740..0edaf9d1a38 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts @@ -11,8 +11,8 @@ import { EntityId, LegacySchoolDo, SchoolFeatures, UserDO, UserLoginMigrationDO import { UserLoginMigrationRepo } from '@shared/repo'; import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { + UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, } from '../loggable'; import { UserLoginMigrationService } from './user-login-migration.service'; @@ -523,34 +523,25 @@ describe(UserLoginMigrationService.name, () => { describe('setMigrationMandatory', () => { describe('when migration is set to mandatory', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ startedAt: mockedDate, mandatorySince: undefined, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigrationDO); - userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); + userLoginMigrationRepo.save.mockResolvedValue(userLoginMigration); return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, }; }; it('should call save the user login migration', async () => { - const { schoolId, userLoginMigrationDO } = setup(); + const { userLoginMigration } = setup(); - await service.setMigrationMandatory(schoolId, true); + await service.setMigrationMandatory(userLoginMigration, true); expect(userLoginMigrationRepo.save).toHaveBeenCalledWith({ - ...userLoginMigrationDO, + ...userLoginMigration, mandatorySince: mockedDate, }); }); @@ -558,65 +549,76 @@ describe(UserLoginMigrationService.name, () => { describe('when migration is set to optional', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ startedAt: mockedDate, mandatorySince: mockedDate, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigrationDO); - userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); - return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, }; }; it('should call save the user login migration', async () => { - const { schoolId, userLoginMigrationDO } = setup(); + const { userLoginMigration } = setup(); - await service.setMigrationMandatory(schoolId, false); + await service.setMigrationMandatory(userLoginMigration, false); expect(userLoginMigrationRepo.save).toHaveBeenCalledWith({ - ...userLoginMigrationDO, + ...userLoginMigration, mandatorySince: undefined, }); }); }); - describe('when migration could not be found', () => { + describe('when the grace period for the user login migration is expired', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); + const dateInThePast: Date = new Date(mockedDate.getTime() - 100); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + closedAt: dateInThePast, + finishedAt: dateInThePast, + }); - const targetSystemId: EntityId = new ObjectId().toHexString(); + return { + userLoginMigration, + dateInThePast, + }; + }; - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, - startedAt: mockedDate, + it('should not save the user login migration again', async () => { + const { userLoginMigration } = setup(); + + await expect(service.setMigrationMandatory({ ...userLoginMigration }, true)).rejects.toThrow(); + + expect(userLoginMigrationRepo.save).not.toHaveBeenCalled(); + }); + + it('should return throw an error', async () => { + const { userLoginMigration, dateInThePast } = setup(); + + await expect(service.setMigrationMandatory({ ...userLoginMigration }, true)).rejects.toThrow( + new UserLoginMigrationGracePeriodExpiredLoggableException(userLoginMigration.id as string, dateInThePast) + ); + }); + }); + + describe('when migration is closed', () => { + const setup = () => { + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ + closedAt: new Date(2023, 5), }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, }; }; - it('should throw UserLoginMigrationLoggableException ', async () => { - const { schoolId } = setup(); + it('should throw a UserLoginMigrationAlreadyClosedLoggableException', async () => { + const { userLoginMigration } = setup(); - const func = async () => service.setMigrationMandatory(schoolId, true); + const func = async () => service.setMigrationMandatory(userLoginMigration, true); - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); + await expect(func).rejects.toThrow(UserLoginMigrationAlreadyClosedLoggableException); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts index 04f74f8408c..459b163f119 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts @@ -6,8 +6,8 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId, LegacySchoolDo, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; import { + UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, } from '../loggable'; @Injectable() @@ -47,12 +47,14 @@ export class UserLoginMigrationService { return updatedUserLoginMigration; } - public async setMigrationMandatory(schoolId: string, mandatory: boolean): Promise { - // this.checkGracePeriod(userLoginMigration); - let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); + public async setMigrationMandatory( + userLoginMigration: UserLoginMigrationDO, + mandatory: boolean + ): Promise { + this.checkGracePeriod(userLoginMigration); - if (!userLoginMigration) { - throw new UserLoginMigrationNotFoundLoggableException(schoolId); + if (userLoginMigration.closedAt) { + throw new UserLoginMigrationAlreadyClosedLoggableException(userLoginMigration.closedAt, userLoginMigration.id); } if (mandatory) { diff --git a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts index 3fcecce5196..60be2ca956b 100644 --- a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts @@ -38,7 +38,7 @@ export class RestartUserLoginMigrationUc { await this.schoolMigrationService.unmarkOutdatedUsers(updatedUserLoginMigration); - this.logger.info(new UserLoginMigrationStartLoggable(userId, updatedUserLoginMigration.id as string)); + this.logger.info(new UserLoginMigrationStartLoggable(userId, updatedUserLoginMigration.id)); return updatedUserLoginMigration; } diff --git a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts index 0f7c615bfbd..1bf47635d83 100644 --- a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts @@ -1,7 +1,7 @@ import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; import { SchoolNumberMissingLoggableException, @@ -21,7 +21,7 @@ export class StartUserLoginMigrationUc { this.logger.setContext(StartUserLoginMigrationUc.name); } - async startMigration(userId: string, schoolId: string): Promise { + async startMigration(userId: EntityId, schoolId: EntityId): Promise { await this.checkPreconditions(userId, schoolId); let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( @@ -31,18 +31,15 @@ export class StartUserLoginMigrationUc { if (!userLoginMigration) { userLoginMigration = await this.userLoginMigrationService.startMigration(schoolId); - this.logger.info(new UserLoginMigrationStartLoggable(userId, userLoginMigration.id as string)); + this.logger.info(new UserLoginMigrationStartLoggable(userId, userLoginMigration.id)); } else if (userLoginMigration.closedAt) { - throw new UserLoginMigrationAlreadyClosedLoggableException( - userLoginMigration.id as string, - userLoginMigration.closedAt - ); + throw new UserLoginMigrationAlreadyClosedLoggableException(userLoginMigration.closedAt, userLoginMigration.id); } return userLoginMigration; } - async checkPreconditions(userId: string, schoolId: string): Promise { + private async checkPreconditions(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts index 75fffd4a3c2..3416250a67e 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts @@ -6,11 +6,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { - UserLoginMigrationAlreadyClosedLoggableException, - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, -} from '../loggable'; +import { ObjectId } from 'bson'; +import { UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { UserLoginMigrationService } from '../service'; import { ToggleUserLoginMigrationUc } from './toggle-user-login-migration.uc'; @@ -78,13 +75,13 @@ describe(ToggleUserLoginMigrationUc.name, () => { userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migrationBeforeMandatory); userLoginMigrationService.setMigrationMandatory.mockResolvedValueOnce(migrationAfterMandatory); - return { user, school, migrationAfterMandatory }; + return { user, school, migrationAfterMandatory, migrationBeforeMandatory }; }; it('should check permission', async () => { const { user, school } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', true); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, @@ -94,17 +91,21 @@ describe(ToggleUserLoginMigrationUc.name, () => { }); it('should call the service to set a migration mandatory', async () => { - setup(); + const { migrationBeforeMandatory } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', true); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true); - expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith('schoolId', true); + expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith(migrationBeforeMandatory, true); }); it('should return a UserLoginMigration', async () => { const { migrationAfterMandatory } = setup(); - const result: UserLoginMigrationDO = await uc.setMigrationMandatory('userId', 'schoolId', true); + const result: UserLoginMigrationDO = await uc.setMigrationMandatory( + new ObjectId().toHexString(), + new ObjectId().toHexString(), + true + ); expect(result).toEqual(migrationAfterMandatory); }); @@ -126,13 +127,13 @@ describe(ToggleUserLoginMigrationUc.name, () => { userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migrationBeforeOptional); userLoginMigrationService.setMigrationMandatory.mockResolvedValueOnce(migrationAfterOptional); - return { user, school, migrationAfterOptional }; + return { user, school, migrationAfterOptional, migrationBeforeOptional }; }; it('should check permission', async () => { const { user, school } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', false); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), false); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, @@ -142,17 +143,21 @@ describe(ToggleUserLoginMigrationUc.name, () => { }); it('should call the service to set a migration optional', async () => { - setup(); + const { migrationBeforeOptional } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', false); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), false); - expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith('schoolId', false); + expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith(migrationBeforeOptional, false); }); it('should return a UserLoginMigration', async () => { const { migrationAfterOptional } = setup(); - const result: UserLoginMigrationDO = await uc.setMigrationMandatory('userId', 'schoolId', false); + const result: UserLoginMigrationDO = await uc.setMigrationMandatory( + new ObjectId().toHexString(), + new ObjectId().toHexString(), + false + ); expect(result).toEqual(migrationAfterOptional); }); @@ -174,9 +179,9 @@ describe(ToggleUserLoginMigrationUc.name, () => { it('should throw an exception', async () => { setup(); - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(ForbiddenException); + await expect( + uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true) + ).rejects.toThrow(ForbiddenException); }); }); @@ -194,61 +199,9 @@ describe(ToggleUserLoginMigrationUc.name, () => { it('should throw a UserLoginMigrationNotFoundLoggableException', async () => { setup(); - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); - }); - }); - - describe('when the grace period for restarting a migration has expired', () => { - const setup = () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(2023, 6)); - - const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - closedAt: new Date(2023, 5), - finishedAt: new Date(2023, 5), - }); - - const user: User = userFactory.buildWithId(); - - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); - }; - - it('should throw a UserLoginMigrationGracePeriodExpiredLoggableException', async () => { - setup(); - - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(UserLoginMigrationGracePeriodExpiredLoggableException); - }); - }); - - describe('when migration is closed', () => { - const setup = () => { - const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - closedAt: new Date(2023, 5), - }); - - const user: User = userFactory.buildWithId(); - - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); - }; - - it('should throw a UserLoginMigrationAlreadyClosedLoggableException', async () => { - setup(); - - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(UserLoginMigrationAlreadyClosedLoggableException); + await expect( + uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true) + ).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts index bb020379b9c..cb964ff7fa6 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts @@ -1,14 +1,9 @@ import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; import { Injectable } from '@nestjs/common'; -import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { - UserLoginMigrationAlreadyClosedLoggableException, - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationMandatoryLoggable, - UserLoginMigrationNotFoundLoggableException, -} from '../loggable'; +import { UserLoginMigrationMandatoryLoggable, UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { UserLoginMigrationService } from '../service'; @Injectable() @@ -20,7 +15,7 @@ export class ToggleUserLoginMigrationUc { private readonly logger: Logger ) {} - async setMigrationMandatory(userId: string, schoolId: string, mandatory: boolean): Promise { + async setMigrationMandatory(userId: EntityId, schoolId: EntityId, mandatory: boolean): Promise { await this.checkPermission(userId, schoolId); let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( @@ -29,26 +24,16 @@ export class ToggleUserLoginMigrationUc { if (!userLoginMigration) { throw new UserLoginMigrationNotFoundLoggableException(schoolId); - } else if (userLoginMigration.finishedAt && Date.now() >= userLoginMigration.finishedAt.getTime()) { - throw new UserLoginMigrationGracePeriodExpiredLoggableException( - userLoginMigration.id as string, - userLoginMigration.finishedAt - ); - } else if (userLoginMigration.closedAt) { - throw new UserLoginMigrationAlreadyClosedLoggableException( - userLoginMigration.id as string, - userLoginMigration.closedAt - ); - } else { - userLoginMigration = await this.userLoginMigrationService.setMigrationMandatory(schoolId, mandatory); - - this.logger.debug(new UserLoginMigrationMandatoryLoggable(userId, userLoginMigration.id as string, mandatory)); } + userLoginMigration = await this.userLoginMigrationService.setMigrationMandatory(userLoginMigration, mandatory); + + this.logger.debug(new UserLoginMigrationMandatoryLoggable(userId, userLoginMigration.id, mandatory)); + return userLoginMigration; } - async checkPermission(userId: string, schoolId: string): Promise { + private async checkPermission(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts index bebd6b7115a..4179505d009 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts @@ -1,12 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; -import { ProvisioningService } from '@modules/provisioning'; -import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { + ProvisioningService, + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + ProvisioningSystemDto, +} from '@modules/provisioning'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -20,8 +24,7 @@ import { userLoginMigrationDOFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { ExternalSchoolNumberMissingLoggableException } from '../loggable'; -import { InvalidUserLoginMigrationLoggableException } from '../loggable/invalid-user-login-migration.loggable-exception'; +import { ExternalSchoolNumberMissingLoggableException, InvalidUserLoginMigrationLoggableException } from '../loggable'; import { SchoolMigrationService, UserLoginMigrationService, UserMigrationService } from '../service'; import { UserLoginMigrationUc } from './user-login-migration.uc'; @@ -104,7 +107,7 @@ describe(UserLoginMigrationUc.name, () => { describe('getMigrations', () => { describe('when searching for a users migration', () => { const setup = () => { - const userId = 'userId'; + const userId = new ObjectId().toHexString(); const migrations: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ schoolId: 'schoolId', @@ -117,7 +120,7 @@ describe(UserLoginMigrationUc.name, () => { return { userId, migrations }; }; - it('should return a response', async () => { + it('should return a page response with data', async () => { const { userId, migrations } = setup(); const result: Page = await uc.getMigrations(userId, { userId }); @@ -131,14 +134,14 @@ describe(UserLoginMigrationUc.name, () => { describe('when a user has no migration available', () => { const setup = () => { - const userId = 'userId'; + const userId = new ObjectId().toHexString(); userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(null); return { userId }; }; - it('should return a response', async () => { + it('should return a page response without data', async () => { const { userId } = setup(); const result: Page = await uc.getMigrations(userId, { userId }); @@ -152,12 +155,12 @@ describe(UserLoginMigrationUc.name, () => { describe('when searching for other users migrations', () => { const setup = () => { - const userId = 'userId'; + const userId = new ObjectId().toHexString(); return { userId }; }; - it('should return a response', async () => { + it('should throw a forbidden exception', async () => { const { userId } = setup(); const func = async () => uc.getMigrations(userId, { userId: 'otherUserId' }); @@ -172,7 +175,7 @@ describe(UserLoginMigrationUc.name, () => { describe('findUserLoginMigrationBySchool', () => { describe('when searching for an existing user login migration', () => { const setup = () => { - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ schoolId, @@ -209,7 +212,7 @@ describe(UserLoginMigrationUc.name, () => { describe('when a user login migration does not exist', () => { const setup = () => { - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); const user: User = userFactory.buildWithId(); @@ -230,7 +233,7 @@ describe(UserLoginMigrationUc.name, () => { describe('when the authorization fails', () => { const setup = () => { - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); const user: User = userFactory.buildWithId(); diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts index 24442147e5e..8314cbfd455 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts @@ -1,9 +1,7 @@ -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; -import { ProvisioningService } from '@modules/provisioning'; -import { OauthDataDto } from '@modules/provisioning/dto'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { ProvisioningService, OauthDataDto } from '@modules/provisioning'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId, LegacySchoolDo, Page, Permission, User, UserLoginMigrationDO } from '@shared/domain'; From 8f1aea39fd4bed99cb4bed845b05df2228ae1909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:11:46 +0100 Subject: [PATCH 21/23] N21-1488 Fix optional external tool parameter with regex (#4558) --- .../common-tool-validation.service.spec.ts | 178 +++++++++--------- .../service/common-tool-validation.service.ts | 46 +++-- .../api-test/tool-context.api.spec.ts | 55 ++++-- .../context-external-tool-request.mapper.ts | 2 +- ...ontext-external-tool-validation.service.ts | 4 +- .../api-test/tool-school.api.spec.ts | 69 ++++--- .../school-external-tool-request.mapper.ts | 4 +- ...l-external-tool-validation.service.spec.ts | 10 +- ...school-external-tool-validation.service.ts | 5 +- .../factory/external-tool-entity.factory.ts | 2 - 10 files changed, 212 insertions(+), 163 deletions(-) diff --git a/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts b/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts index d3ca547a854..1f76ec01a14 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts @@ -109,104 +109,93 @@ describe('CommonToolValidationService', () => { }); }); - describe('checkForDuplicateParameters', () => { - describe('when given parameters has a case sensitive duplicate', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - parameters: [ - { name: 'nameDuplicate', value: 'value' }, - { name: 'nameDuplicate', value: 'value' }, - ], - }); + describe('checkCustomParameterEntries', () => { + const createTools = ( + externalToolMock?: Partial, + schoolExternalToolMock?: Partial, + contextExternalToolMock?: Partial + ) => { + const externalTool: ExternalTool = new ExternalTool({ + ...externalToolFactory.buildWithId(), + ...externalToolMock, + }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + ...schoolExternalToolFactory.buildWithId(), + ...schoolExternalToolMock, + }); + const schoolExternalToolId = schoolExternalTool.id as string; + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + ...contextExternalToolFactory.buildWithId(), + ...contextExternalToolMock, + }); - return { - schoolExternalTool, - }; + return { + externalTool, + schoolExternalTool, + schoolExternalToolId, + contextExternalTool, }; + }; - it('should throw error', () => { - const { schoolExternalTool } = setup(); - - const func = () => service.checkForDuplicateParameters(schoolExternalTool); - - expect(func).toThrow('tool_param_duplicate'); - }); - }); - - describe('when given parameters has case insensitive duplicate', () => { + describe('when a parameter is a duplicate', () => { const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + const externalTool: ExternalTool = externalToolFactory.buildWithId({ parameters: [ - { name: 'nameDuplicate', value: 'value' }, - { name: 'nameduplicate', value: 'value' }, + customParameterFactory.build({ + name: 'duplicate', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: true, + }), + ], + }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id, + parameters: [ + { name: 'duplicate', value: undefined }, + { name: 'duplicate', value: undefined }, ], }); return { + externalTool, schoolExternalTool, }; }; - it('should throw error when given parameters has case insensitive duplicate', () => { - const { schoolExternalTool } = setup(); + it('should throw error', () => { + const { externalTool, schoolExternalTool } = setup(); - const func = () => service.checkForDuplicateParameters(schoolExternalTool); + const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); expect(func).toThrowError('tool_param_duplicate'); }); }); - describe('when given parameters has no duplicates', () => { + describe('when a parameter is unknown', () => { const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - parameters: [ - { name: 'nameNoDuplicate1', value: 'value' }, - { name: 'nameNoDuplicate2', value: 'value' }, - ], + const externalTool: ExternalTool = externalToolFactory.buildWithId({ + parameters: [], + }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id, + parameters: [{ name: 'unknownParameter', value: undefined }], }); return { + externalTool, schoolExternalTool, }; }; - it('when given parameters has no duplicates should return without error', () => { - const { schoolExternalTool } = setup(); + it('should throw error', () => { + const { externalTool, schoolExternalTool } = setup(); - const func = () => service.checkForDuplicateParameters(schoolExternalTool); + const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); - expect(func).not.toThrowError('tool_param_duplicate'); + expect(func).toThrowError('tool_param_unknown'); }); }); - }); - - describe('checkCustomParameterEntries', () => { - const createTools = ( - externalToolMock?: Partial, - schoolExternalToolMock?: Partial, - contextExternalToolMock?: Partial - ) => { - const externalTool: ExternalTool = new ExternalTool({ - ...externalToolFactory.buildWithId(), - ...externalToolMock, - }); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - ...schoolExternalToolFactory.buildWithId(), - ...schoolExternalToolMock, - }); - const schoolExternalToolId = schoolExternalTool.id as string; - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - ...contextExternalToolFactory.buildWithId(), - ...contextExternalToolMock, - }); - - return { - externalTool, - schoolExternalTool, - schoolExternalToolId, - contextExternalTool, - }; - }; describe('when checking parameter is required', () => { describe('and given parameter is not optional and parameter value is empty', () => { @@ -280,16 +269,23 @@ describe('CommonToolValidationService', () => { describe('when parameter is not school or context', () => { const setup = () => { - const notSchoolParam: CustomParameter = customParameterFactory.build({ - name: 'notSchoolParam', - scope: CustomParameterScope.GLOBAL, - type: CustomParameterType.BOOLEAN, - }); - const { externalTool, schoolExternalTool } = createTools( - { parameters: [notSchoolParam] }, { - parameters: [{ name: 'name', value: 'true' }], + parameters: [ + customParameterFactory.build({ + name: 'notSchoolParam', + scope: CustomParameterScope.GLOBAL, + type: CustomParameterType.BOOLEAN, + }), + customParameterFactory.build({ + name: 'schoolParam', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.BOOLEAN, + }), + ], + }, + { + parameters: [{ name: 'schoolParam', value: 'true' }], } ); @@ -320,7 +316,7 @@ describe('CommonToolValidationService', () => { const { externalTool, schoolExternalTool } = createTools( { parameters: [missingParam] }, { - parameters: [{ name: 'anotherParam', value: 'value' }], + parameters: [], } ); @@ -339,18 +335,26 @@ describe('CommonToolValidationService', () => { }); }); - describe('when parameter is optional but is missing on params', () => { + describe('when parameter is optional and was not defined', () => { const setup = () => { - const param: CustomParameter = customParameterFactory.build({ - name: 'notChecked', - scope: CustomParameterScope.SCHOOL, - isOptional: true, - }); - const { externalTool, schoolExternalTool } = createTools( - { parameters: [param] }, { - parameters: [{ name: 'anotherParam', value: 'value' }], + parameters: [ + customParameterFactory.build({ + name: 'optionalParameter', + scope: CustomParameterScope.SCHOOL, + isOptional: true, + }), + customParameterFactory.build({ + name: 'requiredParameter', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: false, + }), + ], + }, + { + parameters: [{ name: 'requiredParameter', value: 'value' }], } ); @@ -360,7 +364,7 @@ describe('CommonToolValidationService', () => { }; }; - it('should return without error ', () => { + it('should return without error', () => { const { externalTool, schoolExternalTool } = setup(); const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); @@ -385,7 +389,7 @@ describe('CommonToolValidationService', () => { }, undefined, { - parameters: [{ name: 'anotherParam', value: 'value' }], + parameters: [], } ); diff --git a/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts b/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts index 3ee9ee7d465..e6c3a31b288 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts @@ -29,12 +29,25 @@ export class CommonToolValidationService { return isValid; } - public checkForDuplicateParameters(validatableTool: ValidatableTool): void { - const caseInsensitiveNames: string[] = validatableTool.parameters.map(({ name }: CustomParameterEntry) => - name.toLowerCase() + public checkCustomParameterEntries(loadedExternalTool: ExternalTool, validatableTool: ValidatableTool): void { + this.checkForDuplicateParameters(validatableTool); + + const parametersForScope: CustomParameter[] = (loadedExternalTool.parameters ?? []).filter( + (param: CustomParameter) => + (validatableTool instanceof SchoolExternalTool && param.scope === CustomParameterScope.SCHOOL) || + (validatableTool instanceof ContextExternalTool && param.scope === CustomParameterScope.CONTEXT) ); + this.checkForUnknownParameters(validatableTool, parametersForScope); + + this.checkValidityOfParameters(validatableTool, parametersForScope); + } + + private checkForDuplicateParameters(validatableTool: ValidatableTool): void { + const caseInsensitiveNames: string[] = validatableTool.parameters.map(({ name }: CustomParameterEntry) => name); + const uniqueNames: Set = new Set(caseInsensitiveNames); + if (uniqueNames.size !== validatableTool.parameters.length) { throw new ValidationError( `tool_param_duplicate: The tool ${validatableTool.id ?? ''} contains multiple of the same custom parameters.` @@ -42,28 +55,33 @@ export class CommonToolValidationService { } } - public checkCustomParameterEntries(loadedExternalTool: ExternalTool, validatableTool: ValidatableTool): void { - if (loadedExternalTool.parameters) { - for (const param of loadedExternalTool.parameters) { - this.checkScopeAndValidateParameter(validatableTool, param); + private checkForUnknownParameters(validatableTool: ValidatableTool, parametersForScope: CustomParameter[]): void { + for (const entry of validatableTool.parameters) { + const foundParameter: CustomParameter | undefined = parametersForScope.find( + ({ name }: CustomParameter): boolean => name === entry.name + ); + + if (!foundParameter) { + throw new ValidationError( + `tool_param_unknown: The parameter with name ${entry.name} is not part of this tool.` + ); } } } - private checkScopeAndValidateParameter(validatableTool: ValidatableTool, param: CustomParameter): void { - const foundEntry: CustomParameterEntry | undefined = validatableTool.parameters.find( - ({ name }: CustomParameterEntry): boolean => name.toLowerCase() === param.name.toLowerCase() - ); + private checkValidityOfParameters(validatableTool: ValidatableTool, parametersForScope: CustomParameter[]): void { + for (const param of parametersForScope) { + const foundEntry: CustomParameterEntry | undefined = validatableTool.parameters.find( + ({ name }: CustomParameterEntry): boolean => name === param.name + ); - if (param.scope === CustomParameterScope.SCHOOL && validatableTool instanceof SchoolExternalTool) { - this.validateParameter(param, foundEntry); - } else if (param.scope === CustomParameterScope.CONTEXT && validatableTool instanceof ContextExternalTool) { this.validateParameter(param, foundEntry); } } private validateParameter(param: CustomParameter, foundEntry: CustomParameterEntry | undefined): void { this.checkOptionalParameter(param, foundEntry); + if (foundEntry) { this.checkParameterType(foundEntry, param); this.checkParameterRegex(foundEntry, param); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts index eb570130c4e..af14eb379f3 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts @@ -17,9 +17,8 @@ import { userFactory, } from '@shared/testing'; import { ObjectId } from 'bson'; -import { CustomParameterScope, ToolContextType } from '../../../common/enum'; +import { CustomParameterScope, CustomParameterType, ToolContextType } from '../../../common/enum'; import { ExternalToolEntity } from '../../../external-tool/entity'; -import { CustomParameterEntryResponse } from '../../../school-external-tool/controller/dto'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; import { @@ -66,10 +65,27 @@ describe('ToolContextController (API)', () => { const course: Course = courseFactory.buildWithId({ teachers: [teacherUser], school }); - const paramEntry: CustomParameterEntryResponse = { name: 'name', value: 'value' }; + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + parameters: [ + customParameterEntityFactory.build({ + name: 'param1', + scope: CustomParameterScope.CONTEXT, + type: CustomParameterType.STRING, + isOptional: false, + }), + customParameterEntityFactory.build({ + name: 'param2', + scope: CustomParameterScope.CONTEXT, + type: CustomParameterType.BOOLEAN, + isOptional: true, + }), + ], + version: 1, + }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + tool: externalToolEntity, school, - schoolParameters: [paramEntry], + schoolParameters: [], toolVersion: 1, }); @@ -78,7 +94,10 @@ describe('ToolContextController (API)', () => { contextId: course.id, displayName: course.name, contextType: ToolContextType.COURSE, - parameters: [paramEntry], + parameters: [ + { name: 'param1', value: 'value' }, + { name: 'param2', value: '' }, + ], toolVersion: 1, }; @@ -87,30 +106,30 @@ describe('ToolContextController (API)', () => { const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount); - const expected: ContextExternalToolResponse = { - id: expect.any(String), - schoolToolId: postParams.schoolToolId, - contextId: postParams.contextId, - displayName: postParams.displayName, - contextType: postParams.contextType, - parameters: [paramEntry], - toolVersion: postParams.toolVersion, - }; - return { loggedInClient, postParams, - expected, }; }; it('should create a contextExternalTool', async () => { - const { postParams, loggedInClient, expected } = await setup(); + const { postParams, loggedInClient } = await setup(); const response = await loggedInClient.post().send(postParams); expect(response.statusCode).toEqual(HttpStatus.CREATED); - expect(response.body).toEqual(expect.objectContaining(expected)); + expect(response.body).toEqual({ + id: expect.any(String), + schoolToolId: postParams.schoolToolId, + contextId: postParams.contextId, + displayName: postParams.displayName, + contextType: postParams.contextType, + parameters: [ + { name: 'param1', value: 'value' }, + { name: 'param2', value: undefined }, + ], + toolVersion: postParams.toolVersion, + }); }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts index 951559afbcc..8e72f5f540b 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts @@ -25,7 +25,7 @@ export class ContextExternalToolRequestMapper { return customParameterParams.map((customParameterParam: CustomParameterEntryParam) => { return { name: customParameterParam.name, - value: customParameterParam.value, + value: customParameterParam.value || undefined, }; }); } diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts index 2cce83a08b2..b12193eadd5 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts @@ -18,7 +18,7 @@ export class ContextExternalToolValidationService { ) {} async validate(contextExternalTool: ContextExternalTool): Promise { - await this.checkDuplicateInContext(contextExternalTool); + await this.checkDuplicateUsesInContext(contextExternalTool); const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId @@ -29,7 +29,7 @@ export class ContextExternalToolValidationService { this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, contextExternalTool); } - private async checkDuplicateInContext(contextExternalTool: ContextExternalTool) { + private async checkDuplicateUsesInContext(contextExternalTool: ContextExternalTool) { let duplicate: ContextExternalTool[] = await this.contextExternalToolService.findContextExternalTools({ schoolToolRef: contextExternalTool.schoolToolRef, context: contextExternalTool.contextRef, diff --git a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts index 3512e66038e..2cca9819b3a 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts @@ -1,9 +1,11 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, Permission, SchoolEntity, User } from '@shared/domain'; import { accountFactory, + customParameterEntityFactory, externalToolEntityFactory, schoolExternalToolEntityFactory, schoolFactory, @@ -11,9 +13,8 @@ import { UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@modules/server'; import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto/tool-configuration-status.response'; -import { ExternalToolEntity } from '../../../external-tool/entity'; +import { CustomParameterScope, CustomParameterType, ExternalToolEntity } from '../../../external-tool/entity'; import { SchoolExternalToolEntity } from '../../entity'; import { CustomParameterEntryParam, @@ -66,31 +67,31 @@ describe('ToolSchoolController (API)', () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ version: 1, - parameters: [], + parameters: [ + customParameterEntityFactory.build({ + name: 'param1', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: false, + }), + customParameterEntityFactory.build({ + name: 'param2', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.BOOLEAN, + isOptional: true, + }), + ], }); - const paramEntry: CustomParameterEntryParam = { name: 'name', value: 'value' }; const postParams: SchoolExternalToolPostParams = { toolId: externalToolEntity.id, schoolId: school.id, version: 1, - parameters: [paramEntry], - }; - - const schoolExternalToolResponse: SchoolExternalToolResponse = new SchoolExternalToolResponse({ - id: expect.any(String), - name: externalToolEntity.name, - schoolId: postParams.schoolId, - toolId: postParams.toolId, - status: ToolConfigurationStatusResponse.LATEST, - toolVersion: postParams.version, parameters: [ - { - name: paramEntry.name, - value: paramEntry.value, - }, + { name: 'param1', value: 'value' }, + { name: 'param2', value: '' }, ], - }); + }; em.persist([ school, @@ -112,7 +113,7 @@ describe('ToolSchoolController (API)', () => { loggedInClientWithMissingPermission, loggedInClient, postParams, - schoolExternalToolResponse, + externalToolEntity, }; }; @@ -125,12 +126,23 @@ describe('ToolSchoolController (API)', () => { }); it('should create an school external tool', async () => { - const { loggedInClient, postParams, schoolExternalToolResponse } = await setup(); + const { loggedInClient, postParams, externalToolEntity } = await setup(); const response = await loggedInClient.post().send(postParams); expect(response.statusCode).toEqual(HttpStatus.CREATED); - expect(response.body).toEqual(schoolExternalToolResponse); + expect(response.body).toEqual({ + id: expect.any(String), + name: externalToolEntity.name, + schoolId: postParams.schoolId, + toolId: postParams.toolId, + status: ToolConfigurationStatusResponse.LATEST, + toolVersion: postParams.version, + parameters: [ + { name: 'param1', value: 'value' }, + { name: 'param2', value: undefined }, + ], + }); const createdSchoolExternalTool: SchoolExternalToolEntity | null = await em.findOne(SchoolExternalToolEntity, { school: postParams.schoolId, @@ -391,7 +403,14 @@ describe('ToolSchoolController (API)', () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ version: 1, - parameters: [], + parameters: [ + customParameterEntityFactory.build({ + name: 'param1', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: false, + }), + ], }); const externalToolEntity2: ExternalToolEntity = externalToolEntityFactory.buildWithId({ version: 1, @@ -422,7 +441,7 @@ describe('ToolSchoolController (API)', () => { accountWithMissingPermission ); - const paramEntry: CustomParameterEntryParam = { name: 'name', value: 'value' }; + const paramEntry: CustomParameterEntryParam = { name: 'param1', value: 'value' }; const postParams: SchoolExternalToolPostParams = { toolId: externalToolEntity.id, schoolId: school.id, @@ -430,7 +449,7 @@ describe('ToolSchoolController (API)', () => { parameters: [paramEntry], }; - const updatedParamEntry: CustomParameterEntryParam = { name: 'name', value: 'updatedValue' }; + const updatedParamEntry: CustomParameterEntryParam = { name: 'param1', value: 'updatedValue' }; const postParamsUpdate: SchoolExternalToolPostParams = { toolId: externalToolEntity.id, schoolId: school.id, diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts index 6617e4ec7fd..eff05c092cb 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { CustomParameterEntry } from '../../common/domain'; import { CustomParameterEntryParam, SchoolExternalToolPostParams } from '../controller/dto'; import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; -import { CustomParameterEntry } from '../../common/domain'; @Injectable() export class SchoolExternalToolRequestMapper { @@ -20,7 +20,7 @@ export class SchoolExternalToolRequestMapper { return customParameterParams.map((customParameterParam: CustomParameterEntryParam) => { return { name: customParameterParam.name, - value: customParameterParam.value, + value: customParameterParam.value || undefined, }; }); } diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts index 6b4f69d6686..1f2ba7f5eb9 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts @@ -4,9 +4,9 @@ import { externalToolFactory, schoolExternalToolFactory } from '@shared/testing/ import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; describe('SchoolExternalToolValidationService', () => { let module: TestingModule; @@ -82,14 +82,6 @@ describe('SchoolExternalToolValidationService', () => { expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); - it('should call commonToolValidationService.checkForDuplicateParameters', async () => { - const { schoolExternalTool } = setup(); - - await service.validate(schoolExternalTool); - - expect(commonToolValidationService.checkForDuplicateParameters).toHaveBeenCalledWith(schoolExternalTool); - }); - it('should call commonToolValidationService.checkCustomParameterEntries', async () => { const { schoolExternalTool } = setup(); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts index 3474aa90f5e..899055e321f 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts @@ -3,8 +3,8 @@ import { ValidationError } from '@shared/common'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; -import { SchoolExternalTool } from '../domain'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { SchoolExternalTool } from '../domain'; @Injectable() export class SchoolExternalToolValidationService { @@ -15,13 +15,12 @@ export class SchoolExternalToolValidationService { ) {} async validate(schoolExternalTool: SchoolExternalTool): Promise { - this.commonToolValidationService.checkForDuplicateParameters(schoolExternalTool); - const loadedExternalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); if (!this.toolFeatures.toolStatusWithoutVersions) { this.checkVersionMatch(schoolExternalTool.toolVersion, loadedExternalTool.version); } + this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, schoolExternalTool); } diff --git a/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts index 32c077e2529..39ff2662cf7 100644 --- a/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts @@ -82,8 +82,6 @@ export const customParameterEntityFactory = BaseFactory.define Date: Wed, 15 Nov 2023 20:31:58 +0100 Subject: [PATCH 22/23] BC-5629 batch deletion mechanism (#4521) * first commit * add some tests * add test cases and services * add new (almost empty for now) batch deletion app * refactor config vars * add optional env var for specifying delay between the API calls * add usecases and test cases * fix importing * add type in uc * fix import * add references service that'll load all the references to the data we want to delete * fix most of issue form review * add deletion API client with just a single method for now that allows for sending a deletion request * refactor the env vars for configurting the Admin API * add exporting DeletionClientConfig * move references service to the deletion module * delete unused code * add batch deletion service that makes it possible ot queue deletion for many references at once * move some parts of the interface to the interface subdir * add an interface for the batch deletion summary * move some interfaces to a separate subdir * refactor the batch deletion summary interface * add uc for the batch deletion * remove unused annotation * refactor deletion client implementation * add batch deletion service implementation * add UC for the batch deletion * add a console app for the deletion module and a console command to manage deletion requests queue * remove no longer used app, add param to make it possible to define delay between the client calls for the case one would like to queue many thousands of deletion requests at once * remove no longer used separate batch-deletion module (it became a part of the main deletion module) * fix invalid key * remove no longer used config vars * remove no longer used commands * remove no longer used Nest cli config * remove no longer used code * change name of the method that prepares default headers * add builders for most of the interfaces * add builders for the remaining interfaces * add type in catch clause * do some adjustments, move PushDeletionRequestsOptions interface to a separate file * remove unused import * rollback * remove unnecessary indent * remove unnecessary indents * remove empty line * remove repeated imports * refactor some imports to omit calling Configuration.get() on every subpackage import * add builder for the DeletionRequestOutput class * add unit tests for the batch deletion service * add unit tests for the BatchDeletionUc * modify env keys for the Admin API client configuration, refactor the way the deletion module's console is bootstrapped * fix invalid import, remove unused undefined arg * add comment to ignore console.ts file for coverage * move deletion client config interface to a separate file, refactor function that prepares current config, add unit tests for it * fix invalid import * add more test cases to the deletion client unit tests * change invalid import Co-authored-by: WojciechGrancow <116577704+WojciechGrancow@users.noreply.github.com> * fix invalid import * add builder for the PushDeletionRequestsOptions class, add unit tests for the DeletionQueueConsole * rename the file containing the deletion module console to deletion.console.ts, add coverage exclusion for it for the Sonar coverage analysis * remove deletion.console.ts from the sonar.coverage.exclusions param as it doesn't seem to work anyway * add deletion.console.ts file to the coverage exclusions (another try with different path) * change name of the file containing the deletion console app * fix some imports * move default value for the ADMIN_API_CLIENT object to default.schema.json * move default for the BASE_URL * move Deletion module console app to the apps/ dir * add separate functino to log error and set exit code * add handling of the case that only CR chars are used as a line separators * add use of the BatchDeletionSummaryBuilder in place of an anonymous object creation * fix some imports/exports * refactor console app flow --------- Co-authored-by: WojciechGrancow Co-authored-by: WojciechGrancow <116577704+WojciechGrancow@users.noreply.github.com> --- apps/server/src/apps/deletion-console.app.ts | 31 +++ .../deletion-request-input.builder.spec.ts | 33 +++ .../builder/deletion-request-input.builder.ts | 11 + .../deletion-request-output.builder.spec.ts | 29 +++ .../deletion-request-output.builder.ts | 10 + ...n-request-target-ref-input.builder.spec.ts | 26 +++ ...letion-request-target-ref-input.builder.ts | 7 + .../modules/deletion/client/builder/index.ts | 3 + .../client/deletion-client.config.spec.ts | 41 ++++ .../deletion/client/deletion-client.config.ts | 9 + .../deletion/client/deletion.client.spec.ts | 154 ++++++++++++++ .../deletion/client/deletion.client.ts | 66 ++++++ .../src/modules/deletion/client/index.ts | 3 + .../deletion-client-config.interface.ts | 4 + .../deletion-request-input.interface.ts | 6 + .../deletion-request-output.interface.ts | 4 + ...tion-request-target-ref-input.interface.ts | 4 + .../deletion/client/interface/index.ts | 4 + .../modules/deletion/console/builder/index.ts | 1 + ...sh-delete-requests-options.builder.spec.ts | 43 ++++ .../push-delete-requests-options.builder.ts | 17 ++ .../console/deletion-console.module.ts | 22 ++ .../console/deletion-queue.console.spec.ts | 79 +++++++ .../console/deletion-queue.console.ts | 46 +++++ .../src/modules/deletion/console/index.ts | 1 + .../deletion/console/interface/index.ts | 1 + .../push-delete-requests-options.interface.ts | 6 + apps/server/src/modules/deletion/index.ts | 2 + .../services/batch-deletion.service.spec.ts | 97 +++++++++ .../services/batch-deletion.service.ts | 55 +++++ .../deletion/services/builder/index.ts | 2 + ...eue-deletion-request-input.builder.spec.ts | 27 +++ .../queue-deletion-request-input.builder.ts | 7 + ...ue-deletion-request-output.builder.spec.ts | 46 +++++ .../queue-deletion-request-output.builder.ts | 29 +++ .../src/modules/deletion/services/index.ts | 4 + .../deletion/services/interface/index.ts | 2 + .../queue-deletion-request-input.interface.ts | 5 + ...queue-deletion-request-output.interface.ts | 5 + .../services/references.service.spec.ts | 74 +++++++ .../deletion/services/references.service.ts | 27 +++ .../deletion/uc/batch-deletion.uc.spec.ts | 195 ++++++++++++++++++ .../modules/deletion/uc/batch-deletion.uc.ts | 71 +++++++ ...ch-deletion-summary-detail.builder.spec.ts | 69 +++++++ .../batch-deletion-summary-detail.builder.ts | 8 + .../batch-deletion-summary.builder.spec.ts | 30 +++ .../builder/batch-deletion-summary.builder.ts | 13 ++ .../src/modules/deletion/uc/builder/index.ts | 2 + apps/server/src/modules/deletion/uc/index.ts | 2 + ...batch-deletion-summary-detail.interface.ts | 6 + ...ch-deletion-summary-overall-status.enum.ts | 4 + .../batch-deletion-summary.interface.ts | 9 + .../modules/deletion/uc/interface/index.ts | 3 + config/default.schema.json | 17 ++ nest-cli.json | 9 + package.json | 3 + 56 files changed, 1484 insertions(+) create mode 100644 apps/server/src/apps/deletion-console.app.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts create mode 100644 apps/server/src/modules/deletion/client/builder/index.ts create mode 100644 apps/server/src/modules/deletion/client/deletion-client.config.spec.ts create mode 100644 apps/server/src/modules/deletion/client/deletion-client.config.ts create mode 100644 apps/server/src/modules/deletion/client/deletion.client.spec.ts create mode 100644 apps/server/src/modules/deletion/client/deletion.client.ts create mode 100644 apps/server/src/modules/deletion/client/index.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts create mode 100644 apps/server/src/modules/deletion/client/interface/index.ts create mode 100644 apps/server/src/modules/deletion/console/builder/index.ts create mode 100644 apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts create mode 100644 apps/server/src/modules/deletion/console/deletion-console.module.ts create mode 100644 apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts create mode 100644 apps/server/src/modules/deletion/console/deletion-queue.console.ts create mode 100644 apps/server/src/modules/deletion/console/index.ts create mode 100644 apps/server/src/modules/deletion/console/interface/index.ts create mode 100644 apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts create mode 100644 apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts create mode 100644 apps/server/src/modules/deletion/services/batch-deletion.service.ts create mode 100644 apps/server/src/modules/deletion/services/builder/index.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts create mode 100644 apps/server/src/modules/deletion/services/interface/index.ts create mode 100644 apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts create mode 100644 apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts create mode 100644 apps/server/src/modules/deletion/services/references.service.spec.ts create mode 100644 apps/server/src/modules/deletion/services/references.service.ts create mode 100644 apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/batch-deletion.uc.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/index.ts create mode 100644 apps/server/src/modules/deletion/uc/index.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts diff --git a/apps/server/src/apps/deletion-console.app.ts b/apps/server/src/apps/deletion-console.app.ts new file mode 100644 index 00000000000..cafb137e160 --- /dev/null +++ b/apps/server/src/apps/deletion-console.app.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +import { BootstrapConsole } from 'nestjs-console'; +import { DeletionConsoleModule } from '@modules/deletion'; + +async function run() { + const bootstrap = new BootstrapConsole({ + module: DeletionConsoleModule, + useDecorators: true, + }); + + const app = await bootstrap.init(); + + try { + await app.init(); + + // Execute console application with provided arguments. + await bootstrap.boot(); + } catch (err) { + // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-call + console.error(err); + + // Set the exit code to 1 to indicate a console app failure. + process.exitCode = 1; + } + + // Always close the app, even if some exception + // has been thrown from the console app. + await app.close(); +} + +void run(); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts new file mode 100644 index 00000000000..bd49bb841e6 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts @@ -0,0 +1,33 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestInput } from '../interface'; +import { DeletionRequestInputBuilder } from './deletion-request-input.builder'; + +describe(DeletionRequestInputBuilder.name, () => { + describe(DeletionRequestInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const targetRefDomain = 'school'; + const targetRefId = new ObjectId().toHexString(); + const deleteInMinutes = 43200; + + const expectedOutput: DeletionRequestInput = { + targetRef: { + domain: targetRefDomain, + id: targetRefId, + }, + deleteInMinutes, + }; + + return { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput } = setup(); + + const output = DeletionRequestInputBuilder.build(targetRefDomain, targetRefId, deleteInMinutes); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts new file mode 100644 index 00000000000..28091418065 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts @@ -0,0 +1,11 @@ +import { DeletionRequestInput } from '../interface'; +import { DeletionRequestTargetRefInputBuilder } from './deletion-request-target-ref-input.builder'; + +export class DeletionRequestInputBuilder { + static build(targetRefDomain: string, targetRefId: string, deleteInMinutes?: number): DeletionRequestInput { + return { + targetRef: DeletionRequestTargetRefInputBuilder.build(targetRefDomain, targetRefId), + deleteInMinutes, + }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts new file mode 100644 index 00000000000..399821f33ff --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts @@ -0,0 +1,29 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestOutput } from '../interface'; +import { DeletionRequestOutputBuilder } from './deletion-request-output.builder'; + +describe(DeletionRequestOutputBuilder.name, () => { + describe(DeletionRequestOutputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const expectedOutput: DeletionRequestOutput = { + requestId, + deletionPlannedAt, + }; + + return { requestId, deletionPlannedAt, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { requestId, deletionPlannedAt, expectedOutput } = setup(); + + const output = DeletionRequestOutputBuilder.build(requestId, deletionPlannedAt); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts new file mode 100644 index 00000000000..9192c1a47ce --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts @@ -0,0 +1,10 @@ +import { DeletionRequestOutput } from '../interface'; + +export class DeletionRequestOutputBuilder { + static build(requestId: string, deletionPlannedAt: Date): DeletionRequestOutput { + return { + requestId, + deletionPlannedAt, + }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts new file mode 100644 index 00000000000..74b0631e49d --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts @@ -0,0 +1,26 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestTargetRefInput } from '../interface'; +import { DeletionRequestTargetRefInputBuilder } from './deletion-request-target-ref-input.builder'; + +describe(DeletionRequestTargetRefInputBuilder.name, () => { + describe(DeletionRequestTargetRefInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const domain = 'user'; + const id = new ObjectId().toHexString(); + + const expectedOutput: DeletionRequestTargetRefInput = { domain, id }; + + return { domain, id, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { domain, id, expectedOutput } = setup(); + + const output = DeletionRequestTargetRefInputBuilder.build(domain, id); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts new file mode 100644 index 00000000000..ed3c0219993 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts @@ -0,0 +1,7 @@ +import { DeletionRequestTargetRefInput } from '../interface'; + +export class DeletionRequestTargetRefInputBuilder { + static build(domain: string, id: string): DeletionRequestTargetRefInput { + return { domain, id }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/index.ts b/apps/server/src/modules/deletion/client/builder/index.ts new file mode 100644 index 00000000000..85644a6b2ee --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/index.ts @@ -0,0 +1,3 @@ +export * from './deletion-request-target-ref-input.builder'; +export * from './deletion-request-input.builder'; +export * from './deletion-request-output.builder'; diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts b/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts new file mode 100644 index 00000000000..a3cae21e425 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts @@ -0,0 +1,41 @@ +import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DeletionClientConfig } from './interface'; +import { getDeletionClientConfig } from './deletion-client.config'; + +describe(getDeletionClientConfig.name, () => { + let configBefore: IConfig; + + beforeAll(() => { + configBefore = Configuration.toObject({ plainSecrets: true }); + }); + + afterEach(() => { + Configuration.reset(configBefore); + }); + + describe('when called', () => { + const setup = () => { + const baseUrl = 'http://api-admin:4030'; + const apiKey = '652559c2-93da-42ad-94e1-640e3afbaca0'; + + Configuration.set('ADMIN_API_CLIENT__BASE_URL', baseUrl); + Configuration.set('ADMIN_API_CLIENT__API_KEY', apiKey); + + const expectedConfig: DeletionClientConfig = { + ADMIN_API_CLIENT_BASE_URL: baseUrl, + ADMIN_API_CLIENT_API_KEY: apiKey, + }; + + return { expectedConfig }; + }; + + it('should return config with proper values', () => { + const { expectedConfig } = setup(); + + const config = getDeletionClientConfig(); + + expect(config).toEqual(expectedConfig); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts new file mode 100644 index 00000000000..db5bf7ff226 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -0,0 +1,9 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DeletionClientConfig } from './interface'; + +export const getDeletionClientConfig = (): DeletionClientConfig => { + return { + ADMIN_API_CLIENT_BASE_URL: Configuration.get('ADMIN_API_CLIENT__BASE_URL') as string, + ADMIN_API_CLIENT_API_KEY: Configuration.get('ADMIN_API_CLIENT__API_KEY') as string, + }; +}; diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts new file mode 100644 index 00000000000..096b1f9b082 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -0,0 +1,154 @@ +import { of } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { axiosResponseFactory } from '@shared/testing'; +import { DeletionRequestInputBuilder, DeletionRequestOutputBuilder } from '.'; +import { DeletionRequestOutput } from './interface'; +import { DeletionClient } from './deletion.client'; + +describe(DeletionClient.name, () => { + let module: TestingModule; + let client: DeletionClient; + let httpService: DeepMocked; + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionClient, + { + provide: ConfigService, + useValue: createMock({ + get: jest.fn((key: string) => { + if (key === 'ADMIN_API_CLIENT_BASE_URL') { + return 'http://localhost:4030'; + } + + // Default is for the Admin APIs API Key. + return '6b3df003-61e9-467c-9e6b-579634801896'; + }), + }), + }, + { + provide: HttpService, + useValue: createMock(), + }, + ], + }).compile(); + + client = module.get(DeletionClient); + httpService = module.get(HttpService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('queueDeletionRequest', () => { + describe('when received valid response with expected HTTP status code', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + '6536ce29b595d7c8e5faf200', + new Date('2024-10-15T12:42:50.521Z') + ); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input, output }; + }; + + it('should return proper output', async () => { + const { input, output } = setup(); + + const result = await client.queueDeletionRequest(input); + + expect(result).toEqual(output); + }); + }); + + describe('when received invalid HTTP status code in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build('', new Date()); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 200, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + + describe('when received no requestId in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + '', + new Date('2024-10-15T12:42:50.521Z') + ); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + + describe('when received no deletionPlannedAt in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: { + requestId: '6536ce29b595d7c8e5faf200', + }, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts new file mode 100644 index 00000000000..66bb267d070 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -0,0 +1,66 @@ +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { DeletionRequestInput, DeletionRequestOutput, DeletionClientConfig } from './interface'; + +@Injectable() +export class DeletionClient { + private readonly baseUrl: string; + + private readonly apiKey: string; + + private readonly postDeletionRequestsEndpoint: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService + ) { + this.baseUrl = this.configService.get('ADMIN_API_CLIENT_BASE_URL'); + this.apiKey = this.configService.get('ADMIN_API_CLIENT_API_KEY'); + + // Prepare the POST /deletionRequests endpoint beforehand to not do it on every client call. + this.postDeletionRequestsEndpoint = new URL('/admin/api/v1/deletionRequests', this.baseUrl).toString(); + } + + async queueDeletionRequest(input: DeletionRequestInput): Promise { + const request = this.httpService.post(this.postDeletionRequestsEndpoint, input, this.defaultHeaders()); + + return firstValueFrom(request) + .then((resp: AxiosResponse) => { + // Throw an error if any other status code (other than expected "202 Accepted" is returned). + if (resp.status !== 202) { + throw new Error(`invalid HTTP status code in a response from the server - ${resp.status} instead of 202`); + } + + // Throw an error if server didn't return a requestId in a response (and it is + // required as it gives client the reference to the created deletion request). + if (!resp.data.requestId) { + throw new Error('no valid requestId returned from the server'); + } + + // Throw an error if server didn't return a deletionPlannedAt timestamp so the user + // will not be aware after which date the deletion request's execution will begin. + if (!resp.data.deletionPlannedAt) { + throw new Error('no valid deletionPlannedAt returned from the server'); + } + + return resp.data; + }) + .catch((err: Error) => { + // Throw an error if sending/processing deletion request by the client failed in any way. + throw new Error(`failed to send/process a deletion request: ${err.toString()}`); + }); + } + + private apiKeyHeader() { + return { 'X-Api-Key': this.apiKey }; + } + + private defaultHeaders() { + return { + headers: this.apiKeyHeader(), + }; + } +} diff --git a/apps/server/src/modules/deletion/client/index.ts b/apps/server/src/modules/deletion/client/index.ts new file mode 100644 index 00000000000..fde3db98f3b --- /dev/null +++ b/apps/server/src/modules/deletion/client/index.ts @@ -0,0 +1,3 @@ +export * from './interface'; +export * from './builder'; +export * from './deletion.client'; diff --git a/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts new file mode 100644 index 00000000000..8178515b6d4 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionClientConfig { + ADMIN_API_CLIENT_BASE_URL: string; + ADMIN_API_CLIENT_API_KEY: string; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts new file mode 100644 index 00000000000..4879ce4d972 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts @@ -0,0 +1,6 @@ +import { DeletionRequestTargetRefInput } from './deletion-request-target-ref-input.interface'; + +export interface DeletionRequestInput { + targetRef: DeletionRequestTargetRefInput; + deleteInMinutes?: number; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts new file mode 100644 index 00000000000..b61a372d5a5 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionRequestOutput { + requestId: string; + deletionPlannedAt: Date; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts new file mode 100644 index 00000000000..603bb0c13ec --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionRequestTargetRefInput { + domain: string; + id: string; +} diff --git a/apps/server/src/modules/deletion/client/interface/index.ts b/apps/server/src/modules/deletion/client/interface/index.ts new file mode 100644 index 00000000000..38f0f639731 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/index.ts @@ -0,0 +1,4 @@ +export * from './deletion-client-config.interface'; +export * from './deletion-request-target-ref-input.interface'; +export * from './deletion-request-input.interface'; +export * from './deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/console/builder/index.ts b/apps/server/src/modules/deletion/console/builder/index.ts new file mode 100644 index 00000000000..12fd0997ebe --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/index.ts @@ -0,0 +1 @@ +export * from './push-delete-requests-options.builder'; diff --git a/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts new file mode 100644 index 00000000000..5c83defdd1e --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts @@ -0,0 +1,43 @@ +import { PushDeletionRequestsOptions } from '../interface'; +import { PushDeleteRequestsOptionsBuilder } from './push-delete-requests-options.builder'; + +describe(PushDeleteRequestsOptionsBuilder.name, () => { + describe(PushDeleteRequestsOptionsBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const refsFilePath = '/tmp/ids.txt'; + const targetRefDomain = 'school'; + const deleteInMinutes = 43200; + const callsDelayMs = 100; + + const expectedOutput: PushDeletionRequestsOptions = { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + }; + + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + expectedOutput, + }; + }; + + it('should return valid object with expected values', () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs, expectedOutput } = setup(); + + const output = PushDeleteRequestsOptionsBuilder.build( + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs + ); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts new file mode 100644 index 00000000000..f8ceae9263d --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts @@ -0,0 +1,17 @@ +import { PushDeletionRequestsOptions } from '../interface'; + +export class PushDeleteRequestsOptionsBuilder { + static build( + refsFilePath: string, + targetRefDomain: string, + deleteInMinutes: number, + callsDelayMs: number + ): PushDeletionRequestsOptions { + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + }; + } +} diff --git a/apps/server/src/modules/deletion/console/deletion-console.module.ts b/apps/server/src/modules/deletion/console/deletion-console.module.ts new file mode 100644 index 00000000000..0585b3631da --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-console.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { ConsoleModule } from 'nestjs-console'; +import { ConsoleWriterModule } from '@infra/console'; +import { createConfigModuleOptions } from '@src/config'; +import { DeletionClient } from '../client'; +import { getDeletionClientConfig } from '../client/deletion-client.config'; +import { BatchDeletionService } from '../services'; +import { BatchDeletionUc } from '../uc'; +import { DeletionQueueConsole } from './deletion-queue.console'; + +@Module({ + imports: [ + ConsoleModule, + ConsoleWriterModule, + HttpModule, + ConfigModule.forRoot(createConfigModuleOptions(getDeletionClientConfig)), + ], + providers: [DeletionClient, BatchDeletionService, BatchDeletionUc, DeletionQueueConsole], +}) +export class DeletionConsoleModule {} diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts new file mode 100644 index 00000000000..61f9cf0ff53 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts @@ -0,0 +1,79 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConsoleWriterService } from '@infra/console'; +import { createMock } from '@golevelup/ts-jest'; +import { BatchDeletionUc } from '../uc'; +import { DeletionQueueConsole } from './deletion-queue.console'; +import { PushDeleteRequestsOptionsBuilder } from './builder'; + +describe(DeletionQueueConsole.name, () => { + let module: TestingModule; + let console: DeletionQueueConsole; + let batchDeletionUc: BatchDeletionUc; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionQueueConsole, + { + provide: ConsoleWriterService, + useValue: createMock(), + }, + { + provide: BatchDeletionUc, + useValue: createMock(), + }, + ], + }).compile(); + + console = module.get(DeletionQueueConsole); + batchDeletionUc = module.get(BatchDeletionUc); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('console should be defined', () => { + expect(console).toBeDefined(); + }); + + describe('pushDeletionRequests', () => { + describe('when called with valid options', () => { + const setup = () => { + const refsFilePath = '/tmp/ids.txt'; + const targetRefDomain = 'school'; + const deleteInMinutes = 43200; + const callsDelayMs = 100; + + const options = PushDeleteRequestsOptionsBuilder.build( + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs + ); + + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + options, + }; + }; + + it(`should call ${BatchDeletionUc.name} with proper arguments`, async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs, options } = setup(); + + const spy = jest.spyOn(batchDeletionUc, 'deleteRefsFromTxtFile'); + + await console.pushDeletionRequests(options); + + expect(spy).toBeCalledWith(refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.ts new file mode 100644 index 00000000000..c8b133dba84 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.ts @@ -0,0 +1,46 @@ +import { Console, Command } from 'nestjs-console'; +import { ConsoleWriterService } from '@infra/console'; +import { BatchDeletionUc } from '../uc'; +import { PushDeletionRequestsOptions } from './interface'; + +@Console({ command: 'queue', description: 'Console providing an access to the deletion queue.' }) +export class DeletionQueueConsole { + constructor(private consoleWriter: ConsoleWriterService, private batchDeletionUc: BatchDeletionUc) {} + + @Command({ + command: 'push', + description: 'Push new deletion requests to the deletion queue.', + options: [ + { + flags: '-rfp, --refsFilePath ', + description: 'Path of the file containing all the references to the data that should be deleted.', + required: true, + }, + { + flags: '-trd, --targetRefDomain ', + description: 'Name of the target ref domain.', + required: false, + }, + { + flags: '-dim, --deleteInMinutes ', + description: 'Number of minutes after which the data deletion process should begin.', + required: false, + }, + { + flags: '-cdm, --callsDelayMs ', + description: 'Delay between all the performed client calls, in milliseconds.', + required: false, + }, + ], + }) + async pushDeletionRequests(options: PushDeletionRequestsOptions): Promise { + const summary = await this.batchDeletionUc.deleteRefsFromTxtFile( + options.refsFilePath, + options.targetRefDomain, + options.deleteInMinutes ? Number(options.deleteInMinutes) : undefined, + options.callsDelayMs ? Number(options.callsDelayMs) : undefined + ); + + this.consoleWriter.info(JSON.stringify(summary)); + } +} diff --git a/apps/server/src/modules/deletion/console/index.ts b/apps/server/src/modules/deletion/console/index.ts new file mode 100644 index 00000000000..db47bd8c99f --- /dev/null +++ b/apps/server/src/modules/deletion/console/index.ts @@ -0,0 +1 @@ +export * from './deletion-console.module'; diff --git a/apps/server/src/modules/deletion/console/interface/index.ts b/apps/server/src/modules/deletion/console/interface/index.ts new file mode 100644 index 00000000000..2fcb281430f --- /dev/null +++ b/apps/server/src/modules/deletion/console/interface/index.ts @@ -0,0 +1 @@ +export * from './push-delete-requests-options.interface'; diff --git a/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts b/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts new file mode 100644 index 00000000000..a54f652cf94 --- /dev/null +++ b/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts @@ -0,0 +1,6 @@ +export interface PushDeletionRequestsOptions { + refsFilePath: string; + targetRefDomain: string; + deleteInMinutes: number; + callsDelayMs: number; +} diff --git a/apps/server/src/modules/deletion/index.ts b/apps/server/src/modules/deletion/index.ts index bd89c1e8d84..793f306e7a0 100644 --- a/apps/server/src/modules/deletion/index.ts +++ b/apps/server/src/modules/deletion/index.ts @@ -1,2 +1,4 @@ export * from './deletion.module'; export * from './services'; +export * from './client'; +export * from './console'; diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts new file mode 100644 index 00000000000..640e290af1a --- /dev/null +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts @@ -0,0 +1,97 @@ +import { ObjectId } from 'bson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DeletionClient, DeletionRequestOutput, DeletionRequestOutputBuilder } from '../client'; +import { QueueDeletionRequestInputBuilder, QueueDeletionRequestOutputBuilder } from './builder'; +import { BatchDeletionService } from './batch-deletion.service'; + +describe(BatchDeletionService.name, () => { + let module: TestingModule; + let service: BatchDeletionService; + let deletionClient: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BatchDeletionService, + { + provide: DeletionClient, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BatchDeletionService); + deletionClient = module.get(DeletionClient); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('service should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('queueDeletionRequests', () => { + describe('when called with valid inputs array and a requested delay between the client calls', () => { + describe("when client doesn't throw any error", () => { + const setup = () => { + const inputs = [QueueDeletionRequestInputBuilder.build('user', new ObjectId().toHexString(), 60)]; + + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const queueDeletionRequestOutput: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + requestId, + deletionPlannedAt + ); + + deletionClient.queueDeletionRequest.mockResolvedValueOnce(queueDeletionRequestOutput); + + const expectedOutput = QueueDeletionRequestOutputBuilder.buildSuccess(requestId, deletionPlannedAt); + + const expectedOutputs = [expectedOutput]; + + return { inputs, expectedOutputs }; + }; + + it('should return an output object with successful status info', async () => { + const { inputs, expectedOutputs } = setup(); + + const outputs = await service.queueDeletionRequests(inputs, 1); + + expect(outputs).toStrictEqual(expectedOutputs); + }); + }); + + describe('when client throws an error', () => { + const setup = () => { + const inputs = [QueueDeletionRequestInputBuilder.build('user', new ObjectId().toHexString(), 60)]; + + const error = new Error('connection error'); + + deletionClient.queueDeletionRequest.mockRejectedValueOnce(error); + + const expectedOutput = QueueDeletionRequestOutputBuilder.buildError(error); + + const expectedOutputs = [expectedOutput]; + + return { inputs, expectedOutputs }; + }; + + it('should return an output object with failure status info', async () => { + const { inputs, expectedOutputs } = setup(); + + const outputs = await service.queueDeletionRequests(inputs); + + expect(outputs).toStrictEqual(expectedOutputs); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts new file mode 100644 index 00000000000..d8c14f315a0 --- /dev/null +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { QueueDeletionRequestOutputBuilder } from './builder'; +import { DeletionClient, DeletionRequestInputBuilder } from '../client'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interface'; + +@Injectable() +export class BatchDeletionService { + constructor(private readonly deletionClient: DeletionClient) {} + + async queueDeletionRequests( + inputs: QueueDeletionRequestInput[], + callsDelayMilliseconds?: number + ): Promise { + const outputs: QueueDeletionRequestOutput[] = []; + + // For every provided deletion request input, try to queue it via deletion client. + // In any case, add the result of the trial to the outputs - it will be either a valid + // response in a form of a requestId + deletionPlannedAt values pair or some error + // returned from the client. In any case, every input should be processed. + for (const input of inputs) { + const deletionRequestInput = DeletionRequestInputBuilder.build( + input.targetRefDomain, + input.targetRefId, + input.deleteInMinutes + ); + + try { + // eslint-disable-next-line no-await-in-loop + const deletionRequestOutput = await this.deletionClient.queueDeletionRequest(deletionRequestInput); + + // In case of a successful client response, add the + // requestId + deletionPlannedAt values pair to the outputs. + outputs.push( + QueueDeletionRequestOutputBuilder.buildSuccess( + deletionRequestOutput.requestId, + deletionRequestOutput.deletionPlannedAt + ) + ); + } catch (err) { + // In case of a failure client response, add the full error message to the outputs. + outputs.push(QueueDeletionRequestOutputBuilder.buildError(err as Error)); + } + + // If any delay between the client calls has been requested, "sleep" for the specified amount of time. + if (callsDelayMilliseconds && callsDelayMilliseconds > 0) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, callsDelayMilliseconds); + }); + } + } + + return outputs; + } +} diff --git a/apps/server/src/modules/deletion/services/builder/index.ts b/apps/server/src/modules/deletion/services/builder/index.ts new file mode 100644 index 00000000000..acd85a37989 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/index.ts @@ -0,0 +1,2 @@ +export * from './queue-deletion-request-input.builder'; +export * from './queue-deletion-request-output.builder'; diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts new file mode 100644 index 00000000000..e5d87858156 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts @@ -0,0 +1,27 @@ +import { ObjectId } from 'bson'; +import { QueueDeletionRequestInput } from '../interface'; +import { QueueDeletionRequestInputBuilder } from './queue-deletion-request-input.builder'; + +describe(QueueDeletionRequestInputBuilder.name, () => { + describe(QueueDeletionRequestInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const targetRefDomain = 'user'; + const targetRefId = new ObjectId().toHexString(); + const deleteInMinutes = 60; + + const expectedOutput: QueueDeletionRequestInput = { targetRefDomain, targetRefId, deleteInMinutes }; + + return { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput } = setup(); + + const output = QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefId, deleteInMinutes); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts new file mode 100644 index 00000000000..a7fff2152b9 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts @@ -0,0 +1,7 @@ +import { QueueDeletionRequestInput } from '../interface'; + +export class QueueDeletionRequestInputBuilder { + static build(targetRefDomain: string, targetRefId: string, deleteInMinutes: number): QueueDeletionRequestInput { + return { targetRefDomain, targetRefId, deleteInMinutes }; + } +} diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts new file mode 100644 index 00000000000..cd835a9cf4a --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts @@ -0,0 +1,46 @@ +import { ObjectId } from 'bson'; +import { QueueDeletionRequestOutput } from '../interface'; +import { QueueDeletionRequestOutputBuilder } from './queue-deletion-request-output.builder'; + +describe(QueueDeletionRequestOutputBuilder.name, () => { + describe(QueueDeletionRequestOutputBuilder.buildSuccess.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const expectedOutput: QueueDeletionRequestOutput = { requestId, deletionPlannedAt }; + + return { requestId, deletionPlannedAt, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { requestId, deletionPlannedAt, expectedOutput } = setup(); + + const output = QueueDeletionRequestOutputBuilder.buildSuccess(requestId, deletionPlannedAt); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); + + describe(QueueDeletionRequestOutputBuilder.buildError.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const error = new Error('test error message'); + + const expectedOutput: QueueDeletionRequestOutput = { error: error.toString() }; + + return { error, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { error, expectedOutput } = setup(); + + const output = QueueDeletionRequestOutputBuilder.buildError(error); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts new file mode 100644 index 00000000000..c44e503cbaf --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts @@ -0,0 +1,29 @@ +import { QueueDeletionRequestOutput } from '../interface'; + +export class QueueDeletionRequestOutputBuilder { + private static build(requestId?: string, deletionPlannedAt?: Date, error?: string): QueueDeletionRequestOutput { + const output: QueueDeletionRequestOutput = {}; + + if (requestId) { + output.requestId = requestId; + } + + if (deletionPlannedAt) { + output.deletionPlannedAt = deletionPlannedAt; + } + + if (error) { + output.error = error.toString(); + } + + return output; + } + + static buildSuccess(requestId: string, deletionPlannedAt: Date): QueueDeletionRequestOutput { + return this.build(requestId, deletionPlannedAt); + } + + static buildError(err: Error): QueueDeletionRequestOutput { + return this.build(undefined, undefined, err.toString()); + } +} diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index 9661354718c..52e0b6ba22d 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1 +1,5 @@ +export * from './interface'; +export * from './builder'; +export * from './references.service'; +export * from './batch-deletion.service'; export * from './deletion-request.service'; diff --git a/apps/server/src/modules/deletion/services/interface/index.ts b/apps/server/src/modules/deletion/services/interface/index.ts new file mode 100644 index 00000000000..8a455440798 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/index.ts @@ -0,0 +1,2 @@ +export * from './queue-deletion-request-input.interface'; +export * from './queue-deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts new file mode 100644 index 00000000000..b421943bce9 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts @@ -0,0 +1,5 @@ +export interface QueueDeletionRequestInput { + targetRefDomain: string; + targetRefId: string; + deleteInMinutes: number; +} diff --git a/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts new file mode 100644 index 00000000000..375ff811857 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts @@ -0,0 +1,5 @@ +export interface QueueDeletionRequestOutput { + requestId?: string; + deletionPlannedAt?: Date; + error?: string; +} diff --git a/apps/server/src/modules/deletion/services/references.service.spec.ts b/apps/server/src/modules/deletion/services/references.service.spec.ts new file mode 100644 index 00000000000..26eb6e0c8d7 --- /dev/null +++ b/apps/server/src/modules/deletion/services/references.service.spec.ts @@ -0,0 +1,74 @@ +import fs from 'fs'; +import { ReferencesService } from './references.service'; + +describe(ReferencesService.name, () => { + describe(ReferencesService.loadFromTxtFile.name, () => { + const setup = (mockedFileContent: string) => { + jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockedFileContent); + }; + + describe('when passed a completely empty file (without any content)', () => { + it('should return an empty references array', () => { + setup(''); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([]); + }); + }); + + describe('when passed a file without any references (just some empty lines)', () => { + it('should return an empty references array', () => { + setup('\n\n \n \n\n\n \n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([]); + }); + }); + + describe('when passed a file with 3 references on a few separate lines', () => { + describe('split with CRs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\r653fd3b784ca851b17e9857a\r653fd3b784ca851b17e9857b\n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + + describe('split with LFs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\n653fd3b784ca851b17e9857a\n653fd3b784ca851b17e9857b\n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + + describe('split with CRLFs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\r\n653fd3b784ca851b17e9857a\r\n653fd3b784ca851b17e9857b\r\n\r\n\r\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/references.service.ts b/apps/server/src/modules/deletion/services/references.service.ts new file mode 100644 index 00000000000..991c36c02af --- /dev/null +++ b/apps/server/src/modules/deletion/services/references.service.ts @@ -0,0 +1,27 @@ +import fs from 'fs'; + +export class ReferencesService { + static loadFromTxtFile(filePath: string): string[] { + let fileContent = fs.readFileSync(filePath).toString(); + + // Replace all the CRLF occurrences with just a LF. + fileContent = fileContent.replace(/\r\n?/g, '\n'); + + // Split the whole file content by a line feed (LF) char (\n). + const fileLines = fileContent.split('\n'); + + const references: string[] = []; + + // Iterate over all the file lines and if it contains a valid id (which is + // basically any non-empty string), add it to the loaded references array. + fileLines.forEach((fileLine) => { + const reference = fileLine.trim(); + + if (reference && reference.length > 0) { + references.push(reference); + } + }); + + return references; + } +} diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts new file mode 100644 index 00000000000..7292f36efb0 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts @@ -0,0 +1,195 @@ +import { ObjectId } from 'bson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + BatchDeletionService, + QueueDeletionRequestInputBuilder, + QueueDeletionRequestOutput, + QueueDeletionRequestOutputBuilder, + ReferencesService, +} from '../services'; +import { BatchDeletionSummaryDetail, BatchDeletionSummaryOverallStatus } from './interface'; +import { BatchDeletionSummaryDetailBuilder } from './builder'; +import { BatchDeletionUc } from './batch-deletion.uc'; + +describe(BatchDeletionUc.name, () => { + let module: TestingModule; + let uc: BatchDeletionUc; + let batchDeletionService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BatchDeletionUc, + { + provide: BatchDeletionService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(BatchDeletionUc); + batchDeletionService = module.get(BatchDeletionService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('uc should be defined', () => { + expect(uc).toBeDefined(); + }); + + describe('deleteRefsFromTxtFile', () => { + describe('when called with valid arguments', () => { + describe('when batch deletion service returns an expected amount of outputs', () => { + describe('when only successful executions took place', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + const outputs: QueueDeletionRequestOutput[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + outputs.push(QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date())); + } + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce([outputs[0], outputs[1], outputs[2]]); + + const targetRefDomain = 'school'; + const deleteInMinutes = 60; + + const expectedSummaryFieldsDetails: BatchDeletionSummaryDetail[] = []; + + for (let i = 0; i < targetRefIds.length; i += 1) { + expectedSummaryFieldsDetails.push( + BatchDeletionSummaryDetailBuilder.build( + QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefIds[i], deleteInMinutes), + outputs[i] + ) + ); + } + + const expectedSummaryFields = { + overallStatus: BatchDeletionSummaryOverallStatus.SUCCESS, + successCount: 3, + failureCount: 0, + details: expectedSummaryFieldsDetails, + }; + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields }; + }; + + it('should return proper summary with all the successes and a successful overall status', async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields } = setup(); + + const summary = await uc.deleteRefsFromTxtFile(refsFilePath, targetRefDomain, deleteInMinutes); + + expect(summary.executionTimeMilliseconds).toBeGreaterThan(0); + expect(summary).toMatchObject(expectedSummaryFields); + }); + }); + + describe('when both successful and failed executions took place', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + } + + const targetRefDomain = 'school'; + const deleteInMinutes = 60; + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + const outputs = [ + QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date()), + QueueDeletionRequestOutputBuilder.buildError(new Error('some error occurred...')), + QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date()), + ]; + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce([outputs[0], outputs[1], outputs[2]]); + + const expectedSummaryFieldsDetails: BatchDeletionSummaryDetail[] = []; + + for (let i = 0; i < targetRefIds.length; i += 1) { + expectedSummaryFieldsDetails.push( + BatchDeletionSummaryDetailBuilder.build( + QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefIds[i], deleteInMinutes), + outputs[i] + ) + ); + } + + const expectedSummaryFields = { + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 2, + failureCount: 1, + details: expectedSummaryFieldsDetails, + }; + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields }; + }; + + it('should return proper summary with all the successes and failures', async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields } = setup(); + + const summary = await uc.deleteRefsFromTxtFile(refsFilePath, targetRefDomain, deleteInMinutes); + + expect(summary.executionTimeMilliseconds).toBeGreaterThan(0); + expect(summary).toMatchObject(expectedSummaryFields); + }); + }); + }); + + describe('when batch deletion service returns an invalid amount of outputs', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + } + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + const outputs: QueueDeletionRequestOutput[] = []; + + for (let i = 0; i < targetRefsCount - 1; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + outputs.push(QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date())); + } + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce(outputs); + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath }; + }; + + it('should throw an error', async () => { + const { refsFilePath } = setup(); + + const func = () => uc.deleteRefsFromTxtFile(refsFilePath); + + await expect(func()).rejects.toThrow(); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts new file mode 100644 index 00000000000..258b1b53f65 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { BatchDeletionSummaryBuilder, BatchDeletionSummaryDetailBuilder } from './builder'; +import { + ReferencesService, + BatchDeletionService, + QueueDeletionRequestInput, + QueueDeletionRequestInputBuilder, +} from '../services'; +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from './interface'; + +@Injectable() +export class BatchDeletionUc { + constructor(private readonly batchDeletionService: BatchDeletionService) {} + + async deleteRefsFromTxtFile( + refsFilePath: string, + targetRefDomain = 'user', + deleteInMinutes = 43200, // 43200 minutes = 720 hours = 30 days + callsDelayMilliseconds?: number + ): Promise { + // First, load all the references from the provided text file (with given path). + const refsFromTxtFile = ReferencesService.loadFromTxtFile(refsFilePath); + + const inputs: QueueDeletionRequestInput[] = []; + + // For each reference found in a given file, add it to the inputs + // array (with added targetRefDomain and deleteInMinutes fields). + refsFromTxtFile.forEach((ref) => + inputs.push(QueueDeletionRequestInputBuilder.build(targetRefDomain, ref, deleteInMinutes)) + ); + + // Measure the overall queueing execution time by setting the start... + const startTime = performance.now(); + + const outputs = await this.batchDeletionService.queueDeletionRequests(inputs, callsDelayMilliseconds); + + // ...and end timestamps before and after the batch deletion service method execution. + const endTime = performance.now(); + + // Throw an error if the returned outputs number doesn't match the returned inputs number. + if (outputs.length !== inputs.length) { + throw new Error( + 'invalid result from the batch deletion service - expected to ' + + 'receive the same amount of outputs as the provided inputs, ' + + `instead received ${outputs.length} outputs for ${inputs.length} inputs` + ); + } + + const summary: BatchDeletionSummary = BatchDeletionSummaryBuilder.build(endTime - startTime); + + // Go through every received output and, in case of an error presence increase + // a failure count or, in case of no error, increase a success count. + for (let i = 0; i < outputs.length; i += 1) { + if (outputs[i].error) { + summary.failureCount += 1; + } else { + summary.successCount += 1; + } + + // Also add all the processed inputs and outputs details to the overall summary. + summary.details.push(BatchDeletionSummaryDetailBuilder.build(inputs[i], outputs[i])); + } + + // If no failure has been spotted, assume an overall success. + if (summary.failureCount === 0) { + summary.overallStatus = BatchDeletionSummaryOverallStatus.SUCCESS; + } + + return summary; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts new file mode 100644 index 00000000000..43ea82e86d5 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts @@ -0,0 +1,69 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BatchDeletionSummaryDetail } from '..'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { BatchDeletionSummaryDetailBuilder } from './batch-deletion-summary-detail.builder'; + +describe(BatchDeletionSummaryDetailBuilder.name, () => { + describe(BatchDeletionSummaryDetailBuilder.build.name, () => { + describe('when called with proper arguments for', () => { + describe('a successful output case', () => { + const setup = () => { + const deletionRequestInput: QueueDeletionRequestInput = { + targetRefDomain: 'user', + targetRefId: new ObjectId().toHexString(), + deleteInMinutes: 1440, + }; + + const deletionRequestOutput: QueueDeletionRequestOutput = { + requestId: new ObjectId().toHexString(), + deletionPlannedAt: new Date(), + }; + + const expectedOutput: BatchDeletionSummaryDetail = { + input: deletionRequestInput, + output: deletionRequestOutput, + }; + + return { deletionRequestInput, deletionRequestOutput, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { deletionRequestInput, deletionRequestOutput, expectedOutput } = setup(); + + const output = BatchDeletionSummaryDetailBuilder.build(deletionRequestInput, deletionRequestOutput); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + + describe('an error output case', () => { + const setup = () => { + const deletionRequestInput: QueueDeletionRequestInput = { + targetRefDomain: 'user', + targetRefId: new ObjectId().toHexString(), + deleteInMinutes: 1440, + }; + + const deletionRequestOutput: QueueDeletionRequestOutput = { + error: 'some error occurred...', + }; + + const expectedOutput: BatchDeletionSummaryDetail = { + input: deletionRequestInput, + output: deletionRequestOutput, + }; + + return { deletionRequestInput, deletionRequestOutput, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { deletionRequestInput, deletionRequestOutput, expectedOutput } = setup(); + + const output = BatchDeletionSummaryDetailBuilder.build(deletionRequestInput, deletionRequestOutput); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts new file mode 100644 index 00000000000..9ebbce66171 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts @@ -0,0 +1,8 @@ +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { BatchDeletionSummaryDetail } from '../interface'; + +export class BatchDeletionSummaryDetailBuilder { + static build(input: QueueDeletionRequestInput, output: QueueDeletionRequestOutput): BatchDeletionSummaryDetail { + return { input, output }; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts new file mode 100644 index 00000000000..a2b534602d4 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts @@ -0,0 +1,30 @@ +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; +import { BatchDeletionSummaryBuilder } from './batch-deletion-summary.builder'; + +describe(BatchDeletionSummaryBuilder.name, () => { + describe(BatchDeletionSummaryBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const executionTimeMilliseconds = 142; + + const expectedOutput: BatchDeletionSummary = { + executionTimeMilliseconds: 142, + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 0, + failureCount: 0, + details: [], + }; + + return { executionTimeMilliseconds, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { executionTimeMilliseconds, expectedOutput } = setup(); + + const output = BatchDeletionSummaryBuilder.build(executionTimeMilliseconds); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts new file mode 100644 index 00000000000..57fa2bcccd9 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts @@ -0,0 +1,13 @@ +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; + +export class BatchDeletionSummaryBuilder { + static build(executionTimeMilliseconds: number): BatchDeletionSummary { + return { + executionTimeMilliseconds, + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 0, + failureCount: 0, + details: [], + }; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/index.ts b/apps/server/src/modules/deletion/uc/builder/index.ts new file mode 100644 index 00000000000..46733980f94 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/index.ts @@ -0,0 +1,2 @@ +export * from './batch-deletion-summary-detail.builder'; +export * from './batch-deletion-summary.builder'; diff --git a/apps/server/src/modules/deletion/uc/index.ts b/apps/server/src/modules/deletion/uc/index.ts new file mode 100644 index 00000000000..cf74de969e5 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/index.ts @@ -0,0 +1,2 @@ +export * from './interface'; +export * from './batch-deletion.uc'; diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts new file mode 100644 index 00000000000..4fe99c13fad --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts @@ -0,0 +1,6 @@ +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; + +export interface BatchDeletionSummaryDetail { + input: QueueDeletionRequestInput; + output: QueueDeletionRequestOutput; +} diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts new file mode 100644 index 00000000000..4ae91bdf70e --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts @@ -0,0 +1,4 @@ +export const enum BatchDeletionSummaryOverallStatus { + SUCCESS = 'success', + FAILURE = 'failure', +} diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts new file mode 100644 index 00000000000..ce633e164f1 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -0,0 +1,9 @@ +import { BatchDeletionSummaryDetail } from './batch-deletion-summary-detail.interface'; + +export interface BatchDeletionSummary { + executionTimeMilliseconds: number; + overallStatus: string; + successCount: number; + failureCount: number; + details: BatchDeletionSummaryDetail[]; +} diff --git a/apps/server/src/modules/deletion/uc/interface/index.ts b/apps/server/src/modules/deletion/uc/interface/index.ts index 95786098275..9dcf644f410 100644 --- a/apps/server/src/modules/deletion/uc/interface/index.ts +++ b/apps/server/src/modules/deletion/uc/interface/index.ts @@ -1 +1,4 @@ +export * from './batch-deletion-summary-overall-status.enum'; +export * from './batch-deletion-summary-detail.interface'; +export * from './batch-deletion-summary.interface'; export * from './interfaces'; diff --git a/config/default.schema.json b/config/default.schema.json index b971968a4ae..89d0a328a59 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1343,6 +1343,23 @@ "description": "Number of simultaneously synchronized students, teachers and classes" } } + }, + "ADMIN_API_CLIENT": { + "type": "object", + "description": "Configuration of the schulcloud-server's admin API client.", + "properties": { + "BASE_URL": { + "type": "string", + "description": "Base URL of the Admin API." + }, + "API_KEY": { + "type": "string", + "description": "API key for accessing the Admin API." + } + }, + "default": { + "BASE_URL": "http://localhost:4030" + } } }, "required": [], diff --git a/nest-cli.json b/nest-cli.json index 73dea03c093..8ce5461bb6f 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -45,6 +45,15 @@ "tsConfigPath": "apps/server/tsconfig.app.json" } }, + "deletion-console": { + "type": "application", + "root": "apps/server", + "entryFile": "apps/deletion-console.app", + "sourceRoot": "apps/server/src", + "compilerOptions": { + "tsConfigPath": "apps/server/tsconfig.app.json" + } + }, "files-storage": { "type": "application", "root": "apps/server", diff --git a/package.json b/package.json index 53a0752ee10..4153b7636e7 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,9 @@ "nest:start:console": "nest start console --", "nest:start:console:dev": "nest start console --watch --", "nest:start:console:debug": "nest start console --debug --watch --", + "nest:start:deletion-console": "nest start deletion-console --", + "nest:start:deletion-console:dev": "nest start deletion-console --watch --", + "nest:start:deletion-console:debug": "nest start deletion-console --debug --watch --", "nest:test": "npm run nest:test:cov && npm run nest:lint", "nest:test:all": "jest", "nest:test:unit": "jest \"^((?!\\.api\\.spec\\.ts).)*\\.spec\\.ts$\"", From 7cf730c05447f9e243499c7174ad68ab7afe6ed8 Mon Sep 17 00:00:00 2001 From: Martin Schuhmacher <55735359+MartinSchuhmacher@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:09:00 +0100 Subject: [PATCH 23/23] BC-5405 - Metadata on k8s ressources (#4517) * add specific labels to deployments, cronjobs and pods --- .../templates/amqp-files-deployment.yml.j2 | 14 +++++++++++++ .../api-delete-s3-files-cronjob.yml.j2 | 21 ++++++++++++++++--- .../templates/api-files-deployment.yml.j2 | 14 +++++++++++++ .../templates/api-fwu-deployment.yml.j2 | 14 +++++++++++++ .../templates/deployment.yml.j2 | 14 +++++++++++++ .../preview-generator-deployment.yml.j2 | 14 +++++++++++++ .../preview-generator-scaled-object.yml.j2 | 7 +++++++ .../templates/api-h5p-deployment.yml.j2 | 14 +++++++++++++ .../templates/management-deployment.yml.j2 | 14 +++++++++++++ .../api-ldap-sync-full-cronjob.yml.j2 | 17 +++++++++++++++ .../api-ldap-worker-deployment.yml.j2 | 14 +++++++++++++ .../api-tsp-sync-base-cronjob.yml.j2 | 20 +++++++++++++++--- .../templates/api-tsp-sync-deployment.yml.j2 | 14 +++++++++++++ .../api-tsp-sync-school-cronjob.yml.j2 | 20 +++++++++++++++--- 14 files changed, 202 insertions(+), 9 deletions(-) diff --git a/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 index 17384845b71..e3843effc48 100644 --- a/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: amqp-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: amqp-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ AMQP_FILE_STORAGE_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: amqp-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: amqp-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 index 317d37dcda9..2eb0fdd4094 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 @@ -5,14 +5,18 @@ metadata: labels: app: api cronjob: delete-s3-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: delete-s3-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-delete-s3-files-cronjob spec: concurrencyPolicy: Forbid schedule: "{{ SERVER_FILE_DELETION_CRONJOB_SCHEDULE|default("@hourly", true) }}" jobTemplate: - labels: - app: api - cronjob: delete-s3-files spec: template: spec: @@ -34,3 +38,14 @@ spec: cpu: {{ API_CPU_REQUESTS|default("100m", true) }} memory: {{ API_MEMORY_REQUESTS|default("150Mi", true) }} restartPolicy: OnFailure + metadata: + labels: + app: api + cronjob: delete-s3-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: delete-s3-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 index 727b5a9f4f0..7866e23dcc3 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_FILE_STORAGE_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 index c8d7e6f894c..5fe3f528bde 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-fwu + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-fwu + app.kubernetes.io/component: fwu + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_FWU_LEARNING_CONTENTS_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-fwu + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-fwu + app.kubernetes.io/component: fwu + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 index 0750bf91495..a18102bb55d 100644 --- a/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api + app.kubernetes.io/component: server + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api + app.kubernetes.io/component: server + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 index 51d87b88755..cffa1dc02b8 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: preview-generator + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: preview-generator + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ AMQP_FILE_PREVIEW_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: preview-generator + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: preview-generator + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 index 2f8f9091b8e..6058b1b0ccb 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 @@ -6,6 +6,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: preview-generator + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: preview-generator + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: scaleTargetRef: name: preview-generator-deployment diff --git a/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 b/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 index 4e41f454ed9..e5923a2e1b9 100644 --- a/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-h5p + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-h5p + app.kubernetes.io/component: h5p + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_H5P_EDITOR_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-h5p + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-h5p + app.kubernetes.io/component: h5p + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 index 9d99c672be8..93378069cc9 100644 --- a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: management-deployment + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: management-deployment + app.kubernetes.io/component: management + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_MANAGEMENT_REPLICAS|default("1", true) }} strategy: @@ -20,6 +27,13 @@ spec: metadata: labels: app: management-deployment + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: management-deployment + app.kubernetes.io/component: management + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 index 74cc37d75b6..db852a7081f 100644 --- a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 @@ -4,6 +4,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-ldapsync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-ldapsync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-ldapsync-full-cronjob spec: schedule: "{{ SERVER_LDAP_SYNC_FULL_CRONJOB|default("0 3 * * 3,6", true) }}" @@ -30,3 +37,13 @@ spec: cpu: {{ API_CPU_REQUESTS|default("100m", true) }} memory: {{ API_MEMORY_REQUESTS|default("150Mi", true) }} restartPolicy: OnFailure + metadata: + labels: + app: api-ldapsync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-ldapsync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 index 838009883d4..9f03dbee883 100644 --- a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-worker + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-worker + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_WORKER_REPLICAS|default("2", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-worker + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-worker + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 index f5a3d0751f4..e46dac14330 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 @@ -4,15 +4,19 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-tsp-sync-base-cronjob spec: schedule: "{{ SERVER_TSP_SYNC_BASE_CRONJOB|default("9 3 * * *", true) }}" jobTemplate: spec: template: - metadata: - labels: - app: api-tsp-sync-cronjob spec: containers: - name: api-tsp-sync-base-cronjob @@ -23,3 +27,13 @@ spec: command: ['/bin/sh','-c'] args: ['curl -H "X-API-Key: $SYNC_API_KEY" "http://{{ API_TSP_SYNC_SVC|default("api-tsp-sync-svc", true) }}:3030/api/v1/sync?target=tsp-base" | python3 -m json.tool'] restartPolicy: OnFailure + metadata: + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 index 54595985dd4..614db8cf1f9 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-tsp-sync + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_TSP_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-tsp-sync + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 index a92bd92560e..e47ae85fab1 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 @@ -4,15 +4,19 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-tsp-sync-school-cronjob spec: schedule: "{{ SERVER_TSP_SYNC_SCHOOL_CRONJOB|default("39 3 * * *", true) }}" jobTemplate: spec: template: - metadata: - labels: - app: api-tsp-sync-cronjob spec: containers: - name: api-tsp-sync-school-cronjob @@ -23,3 +27,13 @@ spec: command: ['/bin/sh','-c'] args: ['curl -H "X-API-Key: $SYNC_API_KEY" "http://{{ API_TSP_SYNC_SVC|default("api-tsp-sync-svc", true) }}:3030/api/v1/sync?target=tsp-school" | python3 -m json.tool'] restartPolicy: OnFailure + metadata: + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }}