From dc46eb25e93889d9fb12994202e7b4356a33465f Mon Sep 17 00:00:00 2001 From: Simone Radtke <94017602+SimoneRadtke-Cap@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:14:44 +0200 Subject: [PATCH 01/35] EW-793 - Refactor Common Cartridge Export (#4918) * Adding loggable exceptions * Renaming folder and adjust regarding imports * Adding common cartridge guard for intended use * Restructuring Common Cartridge export mechanism --------- Co-authored-by: Patrick Sachmann <20001160+psachmann@users.noreply.github.com> --- apps/server/src/modules/board/index.ts | 29 ++-- .../common-cartridge-file-builder.spec.ts | 154 ++++++++--------- .../builders/common-cartridge-file-builder.ts | 79 ++++----- ...mon-cartridge-organization-builder.spec.ts | 87 ---------- .../common-cartridge-organization-builder.ts | 82 --------- ...common-cartridge-organization-node.spec.ts | 157 ++++++++++++++++++ .../common-cartridge-organization-node.ts | 71 ++++++++ ...tridge-resource-collection-builder.spec.ts | 74 +++++++++ ...n-cartridge-resource-collection-builder.ts | 16 ++ .../common-cartridge-resource-node.spec.ts | 46 +++++ .../common-cartridge-resource-node.ts | 23 +++ .../export/common-cartridge.enums.ts | 6 +- .../export/common-cartridge.guard.spec.ts | 29 ++++ .../export/common-cartridge.guard.ts | 9 + ...cartridge-organizations-wrapper-element.ts | 53 ++++++ ...mon-cartridge-resources-wrapper-element.ts | 40 +++++ .../common-cartridge-element-factory.spec.ts | 6 +- .../common-cartridge-element-factory.ts | 5 +- .../common-cartridge-element-factory.spec.ts | 6 +- .../common-cartridge-element-factory.ts | 4 +- .../common-cartridge-metadata-element.spec.ts | 30 +++- .../common-cartridge-metadata-element.ts | 14 +- ...mon-cartridge-organization-element.spec.ts | 39 +++-- .../common-cartridge-organization-element.ts | 49 +++--- ...idge-organizations-wrapper-element.spec.ts | 30 +++- ...cartridge-organizations-wrapper-element.ts | 40 +---- ...artridge-resources-wrapper-element.spec.ts | 28 +++- ...mon-cartridge-resources-wrapper-element.ts | 29 +--- .../common-cartridge-element-factory.spec.ts | 6 +- .../common-cartridge-element-factory.ts | 4 +- .../common-cartridge-metadata-element.spec.ts | 31 +++- .../common-cartridge-metadata-element.ts | 14 +- ...mon-cartridge-organization-element.spec.ts | 37 +++-- .../common-cartridge-organization-element.ts | 49 +++--- ...idge-organizations-wrapper-element.spec.ts | 28 +++- ...cartridge-organizations-wrapper-element.ts | 40 +---- ...artridge-resources-wrapper-element.spec.ts | 28 +++- ...mon-cartridge-resources-wrapper-element.ts | 29 +--- ...e-not-supported.loggable-exception.spec.ts | 23 +++ ...t-type-not-supported.loggable-exception.ts | 24 +++ .../export/errors/error.enums.ts | 7 + .../common-cartridge/export/errors/index.ts | 5 + ...e-not-supported.loggable-exception.spec.ts | 23 +++ ...ed-use-not-supported.loggable-exception.ts | 24 +++ ...issing-metadata.loggable-exception.spec.ts | 22 +++ .../missing-metadata.loggable-exception.ts | 23 +++ ...e-not-supported.loggable-exception.spec.ts | 23 +++ ...e-type-not-supported.loggable-exception.ts | 24 +++ ...n-not-supported.loggable-exception.spec.ts | 23 +++ ...ersion-not-supported.loggable-exception.ts | 24 +++ .../common-cartridge-base.interface.ts | 30 ++++ .../common-cartridge-element.interface.ts | 39 +---- .../common-cartridge-resource.interface.ts | 6 - .../export/interfaces/index.ts | 2 + .../export/interfaces/xml-object.interface.ts | 1 + .../common-cartridge-resource-factory.spec.ts | 6 +- .../common-cartridge-resource-factory.ts | 7 +- ...common-cartridge-manifest-resource.spec.ts | 68 ++++++-- .../common-cartridge-manifest-resource.ts | 30 ++-- .../common-cartridge-resource-factory.spec.ts | 6 +- .../common-cartridge-resource-factory.ts | 4 +- ...mon-cartridge-web-content-resource.spec.ts | 70 +++++--- .../common-cartridge-web-content-resource.ts | 39 ++++- ...common-cartridge-web-link-resource.spec.ts | 67 +++++--- .../common-cartridge-web-link-resource.ts | 40 ++++- ...common-cartridge-manifest-resource.spec.ts | 68 ++++++-- .../common-cartridge-manifest-resource.ts | 26 +-- .../common-cartridge-resource-factory.spec.ts | 6 +- .../common-cartridge-resource-factory.ts | 4 +- ...mon-cartridge-web-content-resource.spec.ts | 70 +++++--- .../common-cartridge-web-content-resource.ts | 39 ++++- ...common-cartridge-web-link-resource.spec.ts | 72 +++++--- .../common-cartridge-web-link-resource.ts | 38 ++++- .../export/resources/v1.3.0/index.ts | 2 +- .../common-cartridge/export/utils.spec.ts | 60 +------ .../modules/common-cartridge/export/utils.ts | 20 --- .../src/modules/common-cartridge/index.ts | 10 +- .../common-cartridge-element-props.factory.ts | 33 +++- ...common-cartridge-resource-props.factory.ts | 30 +++- .../src/modules/learnroom/learnroom.module.ts | 4 +- ...=> common-cartridge-export.mapper.spec.ts} | 23 ++- ...r.ts => common-cartridge-export.mapper.ts} | 26 +-- .../common-cartridge-import.mapper.spec.ts | 9 +- .../mapper/common-cartridge-import.mapper.ts | 22 +-- .../common-cartridge-export.service.spec.ts | 20 +-- .../common-cartridge-export.service.ts | 74 ++++----- .../common-cartridge-import.service.ts | 36 ++-- .../service/course-copy.service.spec.ts | 3 +- .../learnroom/uc/course-import.uc.spec.ts | 2 +- .../modules/learnroom/uc/course-import.uc.ts | 2 +- .../modules/learnroom/uc/course.uc.spec.ts | 6 +- .../src/modules/learnroom/uc/course.uc.ts | 4 +- 92 files changed, 1872 insertions(+), 998 deletions(-) delete mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts delete mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.ts create mode 100644 apps/server/src/modules/common-cartridge/export/common-cartridge.guard.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/common-cartridge.guard.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-organizations-wrapper-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-resources-wrapper-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/error.enums.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/index.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.ts create mode 100644 apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-base.interface.ts create mode 100644 apps/server/src/modules/common-cartridge/export/interfaces/xml-object.interface.ts rename apps/server/src/modules/learnroom/mapper/{common-cartridge.mapper.spec.ts => common-cartridge-export.mapper.spec.ts} (96%) rename apps/server/src/modules/learnroom/mapper/{common-cartridge.mapper.ts => common-cartridge-export.mapper.ts} (88%) diff --git a/apps/server/src/modules/board/index.ts b/apps/server/src/modules/board/index.ts index 1148ed77b61..0321fee95c2 100644 --- a/apps/server/src/modules/board/index.ts +++ b/apps/server/src/modules/board/index.ts @@ -1,28 +1,37 @@ -export { BoardModule } from './board.module'; export { BoardConfig } from './board.config'; +export { BoardModule } from './board.module'; +export { AnyElementContentBody, LinkContentBody, RichTextContentBody } from './controller/dto'; export { - BoardNode, - BoardNodeAuthorizable, - BoardExternalReferenceType, + AnyBoardNode, BoardExternalReference, + BoardExternalReferenceType, BoardLayout, + BoardNode, + BoardNodeAuthorizable, BoardNodeFactory, - ColumnBoard, // @modules/authorization/domain/rules/board-node.rule.ts BoardRoles, + Card, + Column, + ColumnBoard, + ContentElementType, + // @modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts + MediaBoard, + SubmissionItem, + UserWithBoardRoles, + isCard, + isColumn, isDrawingElement, + isLinkElement, + isRichTextElement, isSubmissionItem, isSubmissionItemContent, - SubmissionItem, - UserWithBoardRoles, - // @modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts - MediaBoard, } from './domain'; export { + BoardCommonToolService, BoardNodeAuthorizableService, BoardNodeService, - BoardCommonToolService, ColumnBoardService, MediaAvailableLineService, MediaBoardService, diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts index 5c5db130cf5..308f05b2ff8 100644 --- a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts @@ -1,109 +1,95 @@ import { faker } from '@faker-js/faker'; -import AdmZip from 'adm-zip'; import { - CommonCartridgeElementType, - CommonCartridgeIntendedUseType, - CommonCartridgeResourceType, - CommonCartridgeVersion, -} from '../common-cartridge.enums'; -import { CommonCartridgeElementProps } from '../elements/common-cartridge-element-factory'; -import { CommonCartridgeResourceProps } from '../resources/common-cartridge-resource-factory'; -import { CommonCartridgeFileBuilder } from './common-cartridge-file-builder'; -import { CommonCartridgeOrganizationBuilderOptions } from './common-cartridge-organization-builder'; + createCommonCartridgeMetadataElementProps, + createCommonCartridgeOrganizationProps, +} from '../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWebLinkResourceProps } from '../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../elements/common-cartridge-element-factory'; +import { MissingMetadataLoggableException } from '../errors'; +import { CommonCartridgeFileBuilder, CommonCartridgeFileBuilderProps } from './common-cartridge-file-builder'; +import { CommonCartridgeOrganizationNode } from './common-cartridge-organization-node'; describe('CommonCartridgeFileBuilder', () => { - const getFileContentAsString = (zip: AdmZip, path: string): string | undefined => - zip.getEntry(path)?.getData().toString(); + let sut: CommonCartridgeFileBuilder; - describe('build', () => { - describe('when a common cartridge archive has been created', () => { - const setup = async () => { - const metadataProps: CommonCartridgeElementProps = { - type: CommonCartridgeElementType.METADATA, - title: faker.lorem.words(), - creationDate: new Date(), - copyrightOwners: ['John Doe', 'Jane Doe'], - }; - const organizationOptions: CommonCartridgeOrganizationBuilderOptions = { - identifier: faker.string.uuid(), - title: faker.lorem.words(), - }; - const resourceProps: CommonCartridgeResourceProps = { - type: CommonCartridgeResourceType.WEB_CONTENT, - identifier: faker.string.uuid(), - title: faker.lorem.words(), - html: faker.lorem.paragraphs(), - intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, - }; - const builder = new CommonCartridgeFileBuilder({ - version: CommonCartridgeVersion.V_1_1_0, - identifier: faker.string.uuid(), - }); - - builder - .addMetadata(metadataProps) - .addOrganization(organizationOptions) - .addResource(resourceProps) - .addSubOrganization(organizationOptions) - .addResource(resourceProps) - .addSubOrganization(organizationOptions) - .addResource(resourceProps); - - const archive = new AdmZip(await builder.build()); - - return { archive, metadataProps, organizationOptions, resourceProps }; - }; + const builderProps: CommonCartridgeFileBuilderProps = { + version: CommonCartridgeVersion.V_1_1_0, + identifier: faker.string.uuid(), + }; - it('should have a imsmanifest.xml in archive root', async () => { - const { archive } = await setup(); + beforeEach(() => { + sut = new CommonCartridgeFileBuilder(builderProps); + jest.clearAllMocks(); + }); - const manifest = getFileContentAsString(archive, 'imsmanifest.xml'); + describe('addMetadata', () => { + describe('when metadata is added to the CommonCartridgeFileBuilder', () => { + const setup = () => { + const createElementSpy = jest.spyOn(CommonCartridgeElementFactory, 'createElement'); + const metadataProps = createCommonCartridgeMetadataElementProps(); - expect(manifest).toBeDefined(); - }); + return { metadataProps, createElementSpy }; + }; - it('should have included the resource in organization folder', async () => { - const { archive, organizationOptions, resourceProps } = await setup(); + it('should set the metadata element', () => { + const { metadataProps, createElementSpy } = setup(); - const resource = getFileContentAsString( - archive, - `${organizationOptions.identifier}/${resourceProps.identifier}.html` - ); + sut.addMetadata(metadataProps); - expect(resource).toBeDefined(); + expect(createElementSpy).toHaveBeenCalledWith({ ...metadataProps, version: builderProps.version }); }); + }); + }); - it('should have included the resource in sub-organization folder', async () => { - const { archive, organizationOptions, resourceProps } = await setup(); - - const resource = getFileContentAsString( - archive, - `${organizationOptions.identifier}/${organizationOptions.identifier}/${resourceProps.identifier}.html` - ); + describe('createOrganization', () => { + describe('when an organization is created in the CommonCartridgeFileBuilder', () => { + const setup = () => { + const organizationProps = createCommonCartridgeOrganizationProps(); - expect(resource).toBeDefined(); - }); + return { organizationProps }; + }; - it('should have included the resource in sub-sub-organization folder', async () => { - const { archive, organizationOptions, resourceProps } = await setup(); + it('should create and return an organization node', () => { + const { organizationProps } = setup(); - const resource = getFileContentAsString( - archive, - `${organizationOptions.identifier}/${organizationOptions.identifier}/${organizationOptions.identifier}/${resourceProps.identifier}.html` - ); + const organizationNode = sut.createOrganization(organizationProps); - expect(resource).toBeDefined(); + expect(organizationNode).toBeInstanceOf(CommonCartridgeOrganizationNode); }); }); + }); - describe('when metadata has not been provide', () => { - const sut = new CommonCartridgeFileBuilder({ - version: CommonCartridgeVersion.V_1_1_0, - identifier: faker.string.uuid(), + describe('build', () => { + describe('when metadata has not been provided', () => { + it('should throw MissingMetadataLoggableException', () => { + expect(() => { + sut.build(); + }).toThrow(MissingMetadataLoggableException); }); + }); + + describe('when metadata has been provided', () => { + const setup = () => { + const metadataProps = createCommonCartridgeMetadataElementProps(); + const organizationProps = createCommonCartridgeOrganizationProps(); + const resourceProps = createCommonCartridgeWebLinkResourceProps(); + + return { metadataProps, organizationProps, resourceProps }; + }; + + it('should build the common cartridge file', () => { + const { metadataProps, organizationProps, resourceProps } = setup(); + + sut.addMetadata(metadataProps); + + const org = sut.createOrganization(organizationProps); + + org.addResource(resourceProps); + + const result = sut.build(); - it('should throw an error', async () => { - await expect(sut.build()).rejects.toThrow('Metadata is not defined'); + expect(result).toBeDefined(); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts index 0f605b5561d..35e84aa7115 100644 --- a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts @@ -1,80 +1,81 @@ import AdmZip from 'adm-zip'; -import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { + CommonCartridgeElementType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../common-cartridge.enums'; import { CommonCartridgeElementFactory, CommonCartridgeElementProps, } from '../elements/common-cartridge-element-factory'; -import { CommonCartridgeElement, CommonCartridgeResource } from '../interfaces'; +import { MissingMetadataLoggableException } from '../errors'; +import { CommonCartridgeElement } from '../interfaces'; import { CommonCartridgeResourceFactory } from '../resources/common-cartridge-resource-factory'; -import { OmitVersion } from '../utils'; import { - CommonCartridgeOrganizationBuilder, - CommonCartridgeOrganizationBuilderOptions, -} from './common-cartridge-organization-builder'; + CommonCartridgeOrganizationNode, + CommonCartridgeOrganizationNodeProps, +} from './common-cartridge-organization-node'; +import { CommonCartridgeResourceCollectionBuilder } from './common-cartridge-resource-collection-builder'; export type CommonCartridgeFileBuilderProps = { version: CommonCartridgeVersion; identifier: string; }; -export class CommonCartridgeFileBuilder { - private readonly archive: AdmZip = new AdmZip(); +export type CommonCartridgeOrganizationProps = Omit; - private readonly organizationBuilders = new Array(); +export class CommonCartridgeFileBuilder { + private readonly resourcesBuilder: CommonCartridgeResourceCollectionBuilder = + new CommonCartridgeResourceCollectionBuilder(); - private readonly resources = new Array(); + private readonly organizationsRoot: CommonCartridgeOrganizationNode[] = []; - private metadata?: CommonCartridgeElement; + private metadataElement: CommonCartridgeElement | null = null; constructor(private readonly props: CommonCartridgeFileBuilderProps) {} - public addMetadata(props: CommonCartridgeElementProps): CommonCartridgeFileBuilder { - this.metadata = CommonCartridgeElementFactory.createElement({ + public addMetadata(metadataProps: CommonCartridgeElementProps): void { + this.metadataElement = CommonCartridgeElementFactory.createElement({ version: this.props.version, - ...props, + ...metadataProps, }); - - return this; } - public addOrganization( - props: OmitVersion - ): CommonCartridgeOrganizationBuilder { - const builder = new CommonCartridgeOrganizationBuilder( - { ...props, version: this.props.version }, - (resource: CommonCartridgeResource) => this.resources.push(resource) + public createOrganization(organizationProps: CommonCartridgeOrganizationProps): CommonCartridgeOrganizationNode { + const organization = new CommonCartridgeOrganizationNode( + { ...organizationProps, version: this.props.version, type: CommonCartridgeElementType.ORGANIZATION }, + this.resourcesBuilder, + null ); - this.organizationBuilders.push(builder); + this.organizationsRoot.push(organization); - return builder; + return organization; } - public async build(): Promise { - if (!this.metadata) { - throw new Error('Metadata is not defined'); + public build(): Buffer { + if (!this.metadataElement) { + throw new MissingMetadataLoggableException(); } - const organizations = this.organizationBuilders.map((builder) => builder.build()); + const archive = new AdmZip(); + const organizations = this.organizationsRoot.map((organization) => organization.build()); + const resources = this.resourcesBuilder.build(); const manifest = CommonCartridgeResourceFactory.createResource({ type: CommonCartridgeResourceType.MANIFEST, version: this.props.version, identifier: this.props.identifier, - metadata: this.metadata, + metadata: this.metadataElement, organizations, - resources: this.resources, + resources, }); - for (const resources of this.resources) { - if (!resources.canInline()) { - this.archive.addFile(resources.getFilePath(), Buffer.from(resources.getFileContent())); - } - } - - this.archive.addFile(manifest.getFilePath(), Buffer.from(manifest.getFileContent())); + archive.addFile(manifest.getFilePath(), Buffer.from(manifest.getFileContent())); - const buffer = await this.archive.toBufferPromise(); + resources.forEach((resource) => { + archive.addFile(resource.getFilePath(), Buffer.from(resource.getFileContent())); + }); - return buffer; + return archive.toBuffer(); } } diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts deleted file mode 100644 index ba9a36001ae..00000000000 --- a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { faker } from '@faker-js/faker/locale/af_ZA'; -import { createCommonCartridgeWebContentResourcePropsV110 } from '../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../common-cartridge.enums'; -import { CommonCartridgeElement, CommonCartridgeResource } from '../interfaces'; -import { - CommonCartridgeOrganizationBuilder, - CommonCartridgeOrganizationBuilderOptions, -} from './common-cartridge-organization-builder'; - -describe('CommonCartridgeOrganizationBuilder', () => { - describe('build', () => { - describe('when building a Common Cartridge organization with resources', () => { - const setup = () => { - const resources = new Array(); - - const organizationOptions: CommonCartridgeOrganizationBuilderOptions = { - identifier: faker.string.uuid(), - title: faker.lorem.words(), - }; - - const resourceProps = createCommonCartridgeWebContentResourcePropsV110(); - - const sut = new CommonCartridgeOrganizationBuilder( - { - ...organizationOptions, - version: CommonCartridgeVersion.V_1_1_0, - }, - (resource) => resources.push(resource) - ) - .addResource(resourceProps) - .addSubOrganization(organizationOptions) - .addResource(resourceProps) - .addSubOrganization(organizationOptions) - .addResource(resourceProps); - - return { sut, resources }; - }; - - it('should return a common cartridge element', () => { - const { sut, resources } = setup(); - - const element = sut.build(); - - expect(element).toBeInstanceOf(CommonCartridgeElement); - expect(resources.length).toBe(3); - }); - }); - - describe('when building a Common Cartridge organization with items', () => { - const setup = () => { - const resources = new Array(); - - const organizationOptions: CommonCartridgeOrganizationBuilderOptions = { - identifier: faker.string.uuid(), - title: faker.lorem.words(), - }; - - const resourceProps = createCommonCartridgeWebContentResourcePropsV110(); - - const sut = new CommonCartridgeOrganizationBuilder( - { - ...organizationOptions, - version: CommonCartridgeVersion.V_1_1_0, - }, - (resource) => resources.push(resource) - ) - .addResource(resourceProps) - .addSubOrganization(organizationOptions) - .addResource(resourceProps) - .addSubOrganization(organizationOptions) - .addResource(resourceProps) - .addResource(resourceProps); - - return { sut, resources }; - }; - - it('should return a common cartridge element', () => { - const { sut, resources } = setup(); - - const element = sut.build(); - - expect(element).toBeInstanceOf(CommonCartridgeElement); - expect(resources.length).toBe(4); - }); - }); - }); -}); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts deleted file mode 100644 index 20ff6d3e528..00000000000 --- a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { CommonCartridgeElementType, CommonCartridgeVersion } from '../common-cartridge.enums'; -import { CommonCartridgeElementFactory } from '../elements/common-cartridge-element-factory'; -import { CommonCartridgeElement } from '../interfaces/common-cartridge-element.interface'; -import { CommonCartridgeResource } from '../interfaces/common-cartridge-resource.interface'; -import { - CommonCartridgeResourceFactory, - CommonCartridgeResourceProps, -} from '../resources/common-cartridge-resource-factory'; -import { OmitVersionAndFolder } from '../utils'; - -export type CommonCartridgeOrganizationBuilderOptions = - OmitVersionAndFolder; - -type CommonCartridgeOrganizationBuilderOptionsInternal = { - version: CommonCartridgeVersion; - identifier: string; - title: string; - folder?: string; -}; - -export class CommonCartridgeOrganizationBuilder { - private readonly resources: CommonCartridgeResource[] = []; - - private readonly subOrganizations: CommonCartridgeOrganizationBuilder[] = []; - - constructor( - protected readonly options: CommonCartridgeOrganizationBuilderOptionsInternal, - private readonly addResourceToFileBuilder: (resource: CommonCartridgeResource) => void - ) {} - - private get folder(): string { - return this.options.folder ? `${this.options.folder}/${this.options.identifier}` : this.options.identifier; - } - - public addSubOrganization( - options: OmitVersionAndFolder - ): CommonCartridgeOrganizationBuilder { - const subOrganization = new CommonCartridgeOrganizationBuilder( - { ...options, version: this.options.version, folder: this.folder }, - (resource: CommonCartridgeResource) => this.addResourceToFileBuilder(resource) - ); - - this.subOrganizations.push(subOrganization); - - return subOrganization; - } - - public addResource(props: CommonCartridgeResourceProps): CommonCartridgeOrganizationBuilder { - const resource = CommonCartridgeResourceFactory.createResource({ - version: this.options.version, - folder: this.folder, - ...props, - }); - - this.resources.push(resource); - this.addResourceToFileBuilder(resource); - - return this; - } - - public build(): CommonCartridgeElement { - const organizationElement = CommonCartridgeElementFactory.createElement({ - type: CommonCartridgeElementType.ORGANIZATION, - version: this.options.version, - identifier: this.options.identifier, - title: this.options.title, - items: this.buildItems(), - }); - - return organizationElement; - } - - private buildItems(): (CommonCartridgeElement | CommonCartridgeResource)[] { - if (this.resources.length === 1 && this.subOrganizations.length === 0) { - return [...this.resources]; - } - - const items = [...this.resources, ...this.subOrganizations.map((subOrganization) => subOrganization.build())]; - - return items; - } -} diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.spec.ts new file mode 100644 index 00000000000..204ae5d1685 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.spec.ts @@ -0,0 +1,157 @@ +import { createMock } from '@golevelup/ts-jest'; +import { createCommonCartridgeOrganizationNodeProps } from '../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWebLinkResourceProps } from '../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeElement } from '../interfaces'; +import { CommonCartridgeOrganizationNode } from './common-cartridge-organization-node'; +import { CommonCartridgeResourceCollectionBuilder } from './common-cartridge-resource-collection-builder'; + +describe('CommonCartridgeOrganizationNode', () => { + const setupOrganizationNodeProps = () => { + const props = createCommonCartridgeOrganizationNodeProps(); + + return props; + }; + const setupResourcesMock = () => { + const mock = createMock(); + + return mock; + }; + const setupResourceProps = () => { + const resourceProps = createCommonCartridgeWebLinkResourceProps(); + + return resourceProps; + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('folder', () => { + describe('when organization node has no parent', () => { + const setup = () => { + const resourcesMock = setupResourcesMock(); + const props = setupOrganizationNodeProps(); + const sut = new CommonCartridgeOrganizationNode(props, resourcesMock, null); + + return { sut, props }; + }; + + it('should return its own id as folder', () => { + const { sut, props } = setup(); + + const result = sut.folder; + + expect(result).toBe(props.identifier); + }); + }); + + describe('when organization node has parent', () => { + // AI next 15 lines + const setup = () => { + const resourcesMock = setupResourcesMock(); + const props = setupOrganizationNodeProps(); + const parentProps = setupOrganizationNodeProps(); + const parent = new CommonCartridgeOrganizationNode(parentProps, resourcesMock, null); + const sut = new CommonCartridgeOrganizationNode(props, resourcesMock, parent); + + return { sut, props, parentProps }; + }; + + it('should construct folder path from parent and own identifier', () => { + const { sut, props, parentProps } = setup(); + + const result = sut.folder; + + expect(result).toBe(`${parentProps.identifier}/${props.identifier}`); + }); + }); + }); + + describe('createChild', () => { + describe('when creating a child organization node', () => { + const setup = () => { + const resourcesMock = setupResourcesMock(); + const childrenMock = createMock(); + const props = setupOrganizationNodeProps(); + const childProps = setupOrganizationNodeProps(); + const sut = new CommonCartridgeOrganizationNode(props, resourcesMock, null); + + Reflect.set(sut, 'children', childrenMock); + + return { sut, childProps, childrenMock }; + }; + + it('should return a new organization node', () => { + const { sut, childProps } = setup(); + + const result = sut.createChild(childProps); + + expect(result).toBeInstanceOf(CommonCartridgeOrganizationNode); + expect(result).not.toBe(sut); + }); + + it('should add new organization node to children', () => { + const { sut, childProps, childrenMock } = setup(); + + const result = sut.createChild(childProps); + + expect(result).toBeDefined(); + expect(childrenMock.push).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('addResource', () => { + describe('when adding a resource to an organization node', () => { + const setup = () => { + const resourcesMock = setupResourcesMock(); + const childrenMock = createMock(); + const props = setupOrganizationNodeProps(); + const resourceProps = setupResourceProps(); + const sut = new CommonCartridgeOrganizationNode(props, resourcesMock, null); + + return { sut, resourceProps, childrenMock, resourcesMock }; + }; + + it('should call addResource on resource collection builder', () => { + const { sut, resourceProps, resourcesMock } = setup(); + + sut.addResource(resourceProps); + + expect(resourcesMock.addResource).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('build', () => { + describe('when building an organization node', () => { + const setup = () => { + const resourcesMock = setupResourcesMock(); + const props = setupOrganizationNodeProps(); + const sut = new CommonCartridgeOrganizationNode(props, resourcesMock, null); + const childProps = setupOrganizationNodeProps(); + const childNode = sut.createChild(childProps); + const childNodeBuildSpy = jest.spyOn(childNode, 'build'); + + return { sut, childNodeBuildSpy }; + }; + + it('should return an common cartridge element', () => { + const { sut } = setup(); + + const result = sut.build(); + + expect(result).toBeInstanceOf(CommonCartridgeElement); + }); + + it('should build all children', () => { + const { sut, childNodeBuildSpy } = setup(); + + const result = sut.build(); + + expect(result).toBeDefined(); + expect(childNodeBuildSpy).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.ts new file mode 100644 index 00000000000..9185d9db589 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-node.ts @@ -0,0 +1,71 @@ +import { CommonCartridgeElementType } from '../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../elements/common-cartridge-element-factory'; +import { CommonCartridgeOrganizationElementPropsV110 } from '../elements/v1.1.0'; +import { CommonCartridgeOrganizationElementPropsV130 } from '../elements/v1.3.0'; +import { CommonCartridgeElement } from '../interfaces'; +import { CommonCartridgeResourceProps } from '../resources/common-cartridge-resource-factory'; +import type { CommonCartridgeOrganizationProps } from './common-cartridge-file-builder'; +import { CommonCartridgeResourceCollectionBuilder } from './common-cartridge-resource-collection-builder'; +import { CommonCartridgeResourceNode } from './common-cartridge-resource-node'; + +export type CommonCartridgeOrganizationNodeProps = Omit< + CommonCartridgeOrganizationElementPropsV110 | CommonCartridgeOrganizationElementPropsV130, + 'items' +>; + +export class CommonCartridgeOrganizationNode { + private readonly parent: CommonCartridgeOrganizationNode | null = null; + + private readonly children: (CommonCartridgeOrganizationNode | CommonCartridgeResourceNode)[] = []; + + constructor( + private readonly props: CommonCartridgeOrganizationNodeProps, + private readonly resourcesBuilder: CommonCartridgeResourceCollectionBuilder, + parent: CommonCartridgeOrganizationNode | null + ) { + this.parent = parent; + } + + public get folder(): string { + return this.parent ? `${this.parent.folder}/${this.props.identifier}` : this.props.identifier; + } + + public createChild(childProps: CommonCartridgeOrganizationProps): CommonCartridgeOrganizationNode { + const organization = new CommonCartridgeOrganizationNode( + { + ...childProps, + version: this.props.version, + type: CommonCartridgeElementType.ORGANIZATION, + }, + this.resourcesBuilder, + this + ); + + this.children.push(organization); + + return organization; + } + + public addResource(resourceProps: CommonCartridgeResourceProps): void { + const resource = new CommonCartridgeResourceNode( + { + ...resourceProps, + version: this.props.version, + }, + this + ); + + this.children.push(resource); + this.resourcesBuilder.addResource(resource); + } + + public build(): CommonCartridgeElement { + const organization = CommonCartridgeElementFactory.createElement({ + ...this.props, + version: this.props.version, + items: this.children.map((child) => child.build()), + }); + + return organization; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.spec.ts new file mode 100644 index 00000000000..c7c6d1e8020 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.spec.ts @@ -0,0 +1,74 @@ +import { createMock } from '@golevelup/ts-jest'; +import { createCommonCartridgeOrganizationNodeProps } from '../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWebContentResourceProps } from '../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeOrganizationNode } from './common-cartridge-organization-node'; +import { CommonCartridgeResourceCollectionBuilder } from './common-cartridge-resource-collection-builder'; +import { CommonCartridgeResourceNode, CommonCartridgeResourceNodeProps } from './common-cartridge-resource-node'; + +describe('CommonCartridgeResourceCollectionBuilder', () => { + let sut: CommonCartridgeResourceCollectionBuilder; + + const setupResourceNode = () => { + const resourceNodeProps: CommonCartridgeResourceNodeProps = { + ...createCommonCartridgeWebContentResourceProps(), + version: CommonCartridgeVersion.V_1_1_0, + }; + const organizationNodeProps = createCommonCartridgeOrganizationNodeProps(); + const organizationNode = new CommonCartridgeOrganizationNode(organizationNodeProps, sut, null); + const resourceNode = new CommonCartridgeResourceNode(resourceNodeProps, organizationNode); + + return resourceNode; + }; + + beforeEach(() => { + sut = new CommonCartridgeResourceCollectionBuilder(); + jest.clearAllMocks(); + }); + + describe('addResource', () => { + describe('when a resource is added to the CommonCartridgeResourceCollectionBuilder', () => { + const setup = () => { + const resourceNode = setupResourceNode(); + const resourceNodesMock = createMock(); + + Reflect.set(sut, 'resourceNodes', resourceNodesMock); + + return { resourceNode, resourceNodesMock }; + }; + + it('should add the resource node to the collection', () => { + const { resourceNode, resourceNodesMock } = setup(); + + sut.addResource(resourceNode); + + expect(resourceNodesMock.push).toHaveBeenCalledTimes(1); + expect(resourceNodesMock.push).toHaveBeenCalledWith(resourceNode); + }); + }); + }); + + describe('build', () => { + describe('when build method is called', () => { + const setup = () => { + const resourceNode1 = setupResourceNode(); + const resourceNode2 = setupResourceNode(); + + return { resourceNode1, resourceNode2 }; + }; + + it('should return the resource collection', () => { + const { resourceNode1, resourceNode2 } = setup(); + + sut.addResource(resourceNode1); + sut.addResource(resourceNode2); + + const resources = sut.build(); + + expect(resources).toHaveLength(2); + expect(resources).toContainEqual(resourceNode1.build()); + expect(resources).toContainEqual(resourceNode2.build()); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.ts new file mode 100644 index 00000000000..62b4b082480 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-collection-builder.ts @@ -0,0 +1,16 @@ +import { CommonCartridgeResource } from '../interfaces'; +import { CommonCartridgeResourceNode } from './common-cartridge-resource-node'; + +export class CommonCartridgeResourceCollectionBuilder { + private readonly resourceNodes: CommonCartridgeResourceNode[] = []; + + public addResource(resourceNode: CommonCartridgeResourceNode): void { + this.resourceNodes.push(resourceNode); + } + + public build(): CommonCartridgeResource[] { + const resources = this.resourceNodes.map((resource) => resource.build()); + + return resources; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.spec.ts new file mode 100644 index 00000000000..4cf86f6bb60 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.spec.ts @@ -0,0 +1,46 @@ +import { createCommonCartridgeOrganizationNodeProps } from '../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWebLinkResourceProps } from '../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeResource } from '../interfaces'; +import { CommonCartridgeOrganizationNode } from './common-cartridge-organization-node'; +import { CommonCartridgeResourceCollectionBuilder } from './common-cartridge-resource-collection-builder'; +import { CommonCartridgeResourceNode, CommonCartridgeResourceNodeProps } from './common-cartridge-resource-node'; + +describe('CommonCartridgeResourceNode', () => { + let sut: CommonCartridgeResourceNode; + + describe('build', () => { + describe('when build is called', () => { + const setup = () => { + const resourceNodeProps: CommonCartridgeResourceNodeProps = { + ...createCommonCartridgeWebLinkResourceProps(), + version: CommonCartridgeVersion.V_1_1_0, + }; + + const organizationNodeProps = createCommonCartridgeOrganizationNodeProps(); + + const resourceCollectionBuilder: CommonCartridgeResourceCollectionBuilder = + new CommonCartridgeResourceCollectionBuilder(); + + const organizationNode = new CommonCartridgeOrganizationNode( + organizationNodeProps, + resourceCollectionBuilder, + null + ); + + return { resourceNodeProps, organizationNode }; + }; + + it('should return a CommonCartridgeResource', () => { + const { resourceNodeProps, organizationNode } = setup(); + + sut = new CommonCartridgeResourceNode(resourceNodeProps, organizationNode); + + const result = sut.build(); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(CommonCartridgeResource); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.ts new file mode 100644 index 00000000000..912c969a59a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-resource-node.ts @@ -0,0 +1,23 @@ +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeResource } from '../interfaces'; +import { + CommonCartridgeResourceFactory, + CommonCartridgeResourceProps, +} from '../resources/common-cartridge-resource-factory'; +import type { CommonCartridgeOrganizationNode } from './common-cartridge-organization-node'; + +export type CommonCartridgeResourceNodeProps = CommonCartridgeResourceProps & { version: CommonCartridgeVersion }; + +export class CommonCartridgeResourceNode { + private readonly parent: CommonCartridgeOrganizationNode; + + constructor(private readonly props: CommonCartridgeResourceNodeProps, parent: CommonCartridgeOrganizationNode) { + this.parent = parent; + } + + public build(): CommonCartridgeResource { + const resource = CommonCartridgeResourceFactory.createResource({ ...this.props, folder: this.parent.folder }); + + return resource; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts b/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts index 8e474d7c3df..183cc0f31d4 100644 --- a/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts +++ b/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts @@ -21,8 +21,10 @@ export enum CommonCartridgeIntendedUseType { } export enum CommonCartridgeElementType { + MANIFEST = 'manifest', METADATA = 'metadata', ORGANIZATION = 'organization', - RESOURCES_WRAPPER = 'resourceswrapper', - ORGANIZATIONS_WRAPPER = 'organizationswrapper', + ORGANIZATIONS_WRAPPER = 'organizations-wrapper', + RESOURCES_WRAPPER = 'resources-wrapper', + RESOURCE = 'resource', } diff --git a/apps/server/src/modules/common-cartridge/export/common-cartridge.guard.spec.ts b/apps/server/src/modules/common-cartridge/export/common-cartridge.guard.spec.ts new file mode 100644 index 00000000000..0739b6307b3 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/common-cartridge.guard.spec.ts @@ -0,0 +1,29 @@ +import { CommonCartridgeGuard } from './common-cartridge.guard'; + +describe('CommonCartridgeGuard', () => { + describe('checkIntendedUse', () => { + describe('when intended use is supported', () => { + const supportedIntendedUses = ['use1', 'use2', 'use3']; + + it('should not throw an exception', () => { + const intendedUse = 'use1'; + + expect(() => { + CommonCartridgeGuard.checkIntendedUse(intendedUse, supportedIntendedUses); + }).not.toThrow(); + }); + }); + + describe('when intended use is not supported', () => { + const supportedIntendedUses = ['use1', 'use2', 'use3']; + + it('should throw an exception', () => { + const intendedUse = 'use4'; + + expect(() => { + CommonCartridgeGuard.checkIntendedUse(intendedUse, supportedIntendedUses); + }).toThrow(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/common-cartridge.guard.ts b/apps/server/src/modules/common-cartridge/export/common-cartridge.guard.ts new file mode 100644 index 00000000000..2b0a012cef6 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/common-cartridge.guard.ts @@ -0,0 +1,9 @@ +import { IntendedUseNotSupportedLoggableException } from './errors'; + +export class CommonCartridgeGuard { + public static checkIntendedUse(intendedUse: string, supportedIntendedUses: string[]): void { + if (!supportedIntendedUses.includes(intendedUse)) { + throw new IntendedUseNotSupportedLoggableException(intendedUse); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-organizations-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-organizations-wrapper-element.ts new file mode 100644 index 00000000000..e30c80e0d87 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-organizations-wrapper-element.ts @@ -0,0 +1,53 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, XmlObject } from '../../interfaces'; + +export type CommonCartridgeOrganizationsWrapperElementProps = { + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; + version: CommonCartridgeVersion; + items: CommonCartridgeElement[]; +}; + +/** + * This abstract class was created to reduce code duplication and + * keep the SonarCloud code duplication rate below 3%. + */ +export abstract class CommonCartridgeOrganizationsWrapperElement extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeOrganizationsWrapperElementProps) { + super(props); + } + + abstract getSupportedVersion(): CommonCartridgeVersion; + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.ORGANIZATIONS_WRAPPER: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } + } + + private getManifestXmlObjectInternal(): XmlObject { + return { + organization: [ + { + $: { + identifier: 'org-1', + structure: 'rooted-hierarchy', + }, + item: [ + { + $: { + identifier: 'LearningModules', + }, + item: this.props.items.map((items) => + items.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION) + ), + }, + ], + }, + ], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-resources-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-resources-wrapper-element.ts new file mode 100644 index 00000000000..06059be68ea --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/abstract/common-cartridge-resources-wrapper-element.ts @@ -0,0 +1,40 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, XmlObject } from '../../interfaces'; + +export type CommonCartridgeResourcesWrapperElementProps = { + type: CommonCartridgeElementType.RESOURCES_WRAPPER; + version: CommonCartridgeVersion; + items: CommonCartridgeElement[]; +}; + +/** + * This abstract class was created to reduce code duplication and + * keep the SonarCloud code duplication rate below 3%. + */ +export abstract class CommonCartridgeResourcesWrapperElement extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeResourcesWrapperElementProps) { + super(props); + } + + abstract getSupportedVersion(): CommonCartridgeVersion; + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.RESOURCES_WRAPPER: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } + } + + private getManifestXmlObjectInternal(): XmlObject { + return { + resources: [ + { + resource: this.props.items.map((items) => items.getManifestXmlObject(CommonCartridgeElementType.RESOURCE)), + }, + ], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts index b891d1aa49a..2aad5899e57 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts @@ -1,9 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeMetadataElementPropsV110, createCommonCartridgeMetadataElementPropsV130, } from '../../testing/common-cartridge-element-props.factory'; import { CommonCartridgeElementType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { VersionNotSupportedLoggableException } from '../errors'; import { CommonCartridgeElementFactory } from './common-cartridge-element-factory'; import { CommonCartridgeMetadataElementPropsV110, CommonCartridgeMetadataElementV110 } from './v1.1.0'; import { CommonCartridgeMetadataElementV130 } from './v1.3.0'; @@ -36,14 +36,14 @@ describe('CommonCartridgeElementFactory', () => { CommonCartridgeVersion.V_1_4_0, ]; - it('should throw InternalServerErrorException', () => { + it('should throw VersionNotSupportedLoggableException', () => { notSupportedVersions.forEach((version) => { expect(() => CommonCartridgeElementFactory.createElement({ version, type: CommonCartridgeElementType.METADATA, } as CommonCartridgeMetadataElementPropsV110) - ).toThrow(InternalServerErrorException); + ).toThrow(VersionNotSupportedLoggableException); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts index 046f7033fde..7765e79d7ac 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts @@ -1,6 +1,7 @@ import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { VersionNotSupportedLoggableException } from '../errors'; import { CommonCartridgeElement } from '../interfaces'; -import { OmitVersionAndFolder, createVersionNotSupportedError } from '../utils'; +import { OmitVersionAndFolder } from '../utils'; import { CommonCartridgeElementFactoryV110, CommonCartridgeMetadataElementPropsV110, @@ -46,7 +47,7 @@ export class CommonCartridgeElementFactory { case CommonCartridgeVersion.V_1_3_0: return CommonCartridgeElementFactoryV130.createElement(props); default: - throw createVersionNotSupportedError(version); + throw new VersionNotSupportedLoggableException(version); } } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts index 3c43cf3a830..3c4fceabe83 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts @@ -1,4 +1,3 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeMetadataElementPropsV110, createCommonCartridgeOrganizationElementPropsV110, @@ -6,6 +5,7 @@ import { createCommonCartridgeResourcesWrapperElementPropsV110, } from '../../../testing/common-cartridge-element-props.factory'; import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeElementFactoryV110 } from './common-cartridge-element-factory'; import { CommonCartridgeMetadataElementV110 } from './common-cartridge-metadata-element'; import { CommonCartridgeOrganizationElementV110 } from './common-cartridge-organization-element'; @@ -53,9 +53,9 @@ describe('CommonCartridgeElementFactoryV110', () => { notSupportedProps.type = 'not-supported' as CommonCartridgeElementType.METADATA; - it('should throw error', () => { + it('should throw ElementTypeNotSupportedLoggableException', () => { expect(() => CommonCartridgeElementFactoryV110.createElement(notSupportedProps)).toThrow( - InternalServerErrorException + ElementTypeNotSupportedLoggableException ); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts index 88095cf218a..b623cd277f8 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts @@ -1,6 +1,6 @@ import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeElement } from '../../interfaces'; -import { createElementTypeNotSupportedError } from '../../utils'; import { CommonCartridgeMetadataElementPropsV110, CommonCartridgeMetadataElementV110, @@ -39,7 +39,7 @@ export class CommonCartridgeElementFactoryV110 { case CommonCartridgeElementType.RESOURCES_WRAPPER: return new CommonCartridgeResourcesWrapperElementV110(props); default: - throw createElementTypeNotSupportedError(type); + throw new ElementTypeNotSupportedLoggableException(type); } } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts index cd12efa1590..1ab4e0421c4 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts @@ -1,6 +1,6 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeMetadataElementPropsV110 } from '../../../testing/common-cartridge-element-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeMetadataElementV110 } from './common-cartridge-metadata-element'; describe('CommonCartridgeMetadataElementV110', () => { @@ -27,13 +27,15 @@ describe('CommonCartridgeMetadataElementV110', () => { notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; it('should throw error', () => { - expect(() => new CommonCartridgeMetadataElementV110(notSupportedProps)).toThrow(InternalServerErrorException); + expect(() => new CommonCartridgeMetadataElementV110(notSupportedProps)).toThrow( + VersionNotSupportedLoggableException + ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.1', () => { + describe('when creating metadata xml object', () => { const setup = () => { const props = createCommonCartridgeMetadataElementPropsV110(); const sut = new CommonCartridgeMetadataElementV110(props); @@ -41,10 +43,10 @@ describe('CommonCartridgeMetadataElementV110', () => { return { sut, props }; }; - it('should return correct manifest xml object', () => { + it('should return metadata manifest fragment', () => { const { sut, props } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.METADATA); expect(result).toStrictEqual({ schema: 'IMS Common Cartridge', @@ -67,5 +69,21 @@ describe('CommonCartridgeMetadataElementV110', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeMetadataElementPropsV110(); + const sut = new CommonCartridgeMetadataElementV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts index c70cbd2c892..f51b842e011 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts @@ -1,5 +1,6 @@ import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement } from '../../interfaces'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, XmlObject } from '../../interfaces'; export type CommonCartridgeMetadataElementPropsV110 = { type: CommonCartridgeElementType.METADATA; @@ -18,7 +19,16 @@ export class CommonCartridgeMetadataElementV110 extends CommonCartridgeElement { return CommonCartridgeVersion.V_1_1_0; } - public getManifestXmlObject(): Record { + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.METADATA: + return this.getManifestMetadataXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } + } + + private getManifestMetadataXmlObjectInternal() { return { schema: 'IMS Common Cartridge', schemaversion: '1.1.0', diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts index 8e1be0c920f..f42cc140d5d 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts @@ -1,7 +1,7 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeOrganizationElementPropsV110 } from '../../../testing/common-cartridge-element-props.factory'; import { createCommonCartridgeWeblinkResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; import { CommonCartridgeOrganizationElementV110 } from './common-cartridge-organization-element'; @@ -31,25 +31,20 @@ describe('CommonCartridgeOrganizationElementV110', () => { it('should throw error', () => { expect(() => new CommonCartridgeOrganizationElementV110(notSupportedProps)).toThrowError( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.1.0', () => { + describe('when creating organization xml object', () => { const setup = () => { const resourceProps = createCommonCartridgeWeblinkResourcePropsV110(); - - const subOrganization1Props = createCommonCartridgeOrganizationElementPropsV110( - CommonCartridgeResourceFactory.createResource(resourceProps) - ); - + const subOrganization1Props = createCommonCartridgeOrganizationElementPropsV110(); const subOrganization2Props = createCommonCartridgeOrganizationElementPropsV110([ CommonCartridgeResourceFactory.createResource(resourceProps), ]); - const organizationProps = createCommonCartridgeOrganizationElementPropsV110([ CommonCartridgeElementFactory.createElement(subOrganization1Props), CommonCartridgeElementFactory.createElement(subOrganization2Props), @@ -60,10 +55,10 @@ describe('CommonCartridgeOrganizationElementV110', () => { return { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps }; }; - it('should return correct manifest xml object', () => { + it('should return organization manifest fragment', () => { const { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); expect(result).toStrictEqual({ $: { @@ -74,9 +69,9 @@ describe('CommonCartridgeOrganizationElementV110', () => { { $: { identifier: subOrganization1Props.identifier, - identifierref: resourceProps.identifier, }, title: subOrganization1Props.title, + item: [], }, { $: { @@ -97,5 +92,23 @@ describe('CommonCartridgeOrganizationElementV110', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeOrganizationElementPropsV110(); + const sut = new CommonCartridgeOrganizationElementV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrowError( + ElementTypeNotSupportedLoggableException + ); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts index ff304d6ea08..215f68d7a38 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts @@ -1,13 +1,13 @@ import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; -import { createIdentifier } from '../../utils'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, CommonCartridgeResource, XmlObject } from '../../interfaces'; export type CommonCartridgeOrganizationElementPropsV110 = { type: CommonCartridgeElementType.ORGANIZATION; version: CommonCartridgeVersion; identifier: string; title: string; - items: CommonCartridgeResource | Array; + items: CommonCartridgeResource | Array; }; export class CommonCartridgeOrganizationElementV110 extends CommonCartridgeElement { @@ -19,35 +19,32 @@ export class CommonCartridgeOrganizationElementV110 extends CommonCartridgeEleme return CommonCartridgeVersion.V_1_1_0; } - public getManifestXmlObject(): Record { - if (this.props.items instanceof CommonCartridgeResource) { - return { - $: { - identifier: this.identifier, - identifierref: this.props.items.identifier, - }, - title: this.title, - }; + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.ORGANIZATION: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); } + } + + private getManifestXmlObjectInternal(): XmlObject { + const xmlObject = Array.isArray(this.props.items) + ? this.getManifestXmlObjectForMany(this.props.items) + : this.props.items.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); - return { + return xmlObject; + } + + private getManifestXmlObjectForMany(items: Array): XmlObject { + const xmlObject = { $: { identifier: this.identifier, }, title: this.title, - item: this.props.items.map((item) => { - if (item instanceof CommonCartridgeResource) { - return { - $: { - identifier: createIdentifier(), - identifierref: item.identifier, - }, - title: item.title, - }; - } - - return item.getManifestXmlObject(); - }), + item: items.map((item) => item.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION)), }; + + return xmlObject; } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts index fb5ae465a28..2efc3000a3c 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts @@ -1,9 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeOrganizationElementPropsV110, createCommonCartridgeOrganizationsWrapperElementPropsV110, } from '../../../testing/common-cartridge-element-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; import { CommonCartridgeOrganizationsWrapperElementV110 } from './common-cartridge-organizations-wrapper-element'; @@ -31,15 +31,15 @@ describe('CommonCartridgeOrganizationsWrapperElementV110', () => { notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; it('should throw error', () => { - expect(() => new CommonCartridgeOrganizationsWrapperElementV110(notSupportedProps)).toThrowError( - InternalServerErrorException + expect(() => new CommonCartridgeOrganizationsWrapperElementV110(notSupportedProps)).toThrow( + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.1.0', () => { + describe('when creating organization wrapper xml object', () => { const setup = () => { const organizationProps = createCommonCartridgeOrganizationElementPropsV110(); const props = createCommonCartridgeOrganizationsWrapperElementPropsV110([ @@ -50,10 +50,10 @@ describe('CommonCartridgeOrganizationsWrapperElementV110', () => { return { sut, organizationProps }; }; - it('should return correct manifest xml object', () => { + it('should return organization wrapper manifest fragment', () => { const { sut, organizationProps } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATIONS_WRAPPER); expect(result).toStrictEqual({ organization: [ @@ -83,5 +83,21 @@ describe('CommonCartridgeOrganizationsWrapperElementV110', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeOrganizationsWrapperElementPropsV110(); + const sut = new CommonCartridgeOrganizationsWrapperElementV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts index 9d1c44ec85d..564d010b79c 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts @@ -1,39 +1,13 @@ -import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement } from '../../interfaces'; +import { + CommonCartridgeOrganizationsWrapperElement, + CommonCartridgeOrganizationsWrapperElementProps, +} from '../abstract/common-cartridge-organizations-wrapper-element'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; -export type CommonCartridgeOrganizationsWrapperElementPropsV110 = { - type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; - version: CommonCartridgeVersion; - items: CommonCartridgeElement[]; -}; - -export class CommonCartridgeOrganizationsWrapperElementV110 extends CommonCartridgeElement { - constructor(private readonly props: CommonCartridgeOrganizationsWrapperElementPropsV110) { - super(props); - } +export type CommonCartridgeOrganizationsWrapperElementPropsV110 = CommonCartridgeOrganizationsWrapperElementProps; +export class CommonCartridgeOrganizationsWrapperElementV110 extends CommonCartridgeOrganizationsWrapperElement { public getSupportedVersion(): CommonCartridgeVersion { return CommonCartridgeVersion.V_1_1_0; } - - public getManifestXmlObject(): Record { - return { - organization: [ - { - $: { - identifier: 'org-1', - structure: 'rooted-hierarchy', - }, - item: [ - { - $: { - identifier: 'LearningModules', - }, - item: this.props.items.map((items) => items.getManifestXmlObject()), - }, - ], - }, - ], - }; - } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts index 0106eefbee5..ed54df3df8e 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts @@ -1,7 +1,7 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeResourcesWrapperElementPropsV110 } from '../../../testing/common-cartridge-element-props.factory'; import { createCommonCartridgeWeblinkResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; import { CommonCartridgeResourcesWrapperElementV110 } from './common-cartridge-resources-wrapper-element'; @@ -30,14 +30,14 @@ describe('CommonCartridgeResourcesWrapperElementV110', () => { it('should throw error', () => { expect(() => new CommonCartridgeResourcesWrapperElementV110(notSupportedProps)).toThrow( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using common cartridge version 1.1.0', () => { + describe('when creating resources wrapper xml object', () => { const setup = () => { const resourceProps = createCommonCartridgeWeblinkResourcePropsV110(); const props = createCommonCartridgeResourcesWrapperElementPropsV110([ @@ -48,10 +48,10 @@ describe('CommonCartridgeResourcesWrapperElementV110', () => { return { sut, resourceProps }; }; - it('should return correct manifest xml object', () => { + it('should return resources wrapper manifest fragment', () => { const { sut, resourceProps } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.RESOURCES_WRAPPER); expect(result).toStrictEqual({ resources: [ @@ -74,5 +74,21 @@ describe('CommonCartridgeResourcesWrapperElementV110', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeResourcesWrapperElementPropsV110(); + const sut = new CommonCartridgeResourcesWrapperElementV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts index 4048787732a..e179834e99e 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts @@ -1,28 +1,13 @@ -import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement } from '../../interfaces'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { + CommonCartridgeResourcesWrapperElement, + CommonCartridgeResourcesWrapperElementProps, +} from '../abstract/common-cartridge-resources-wrapper-element'; -export type CommonCartridgeResourcesWrapperElementPropsV110 = { - type: CommonCartridgeElementType.RESOURCES_WRAPPER; - version: CommonCartridgeVersion; - items: CommonCartridgeElement[]; -}; - -export class CommonCartridgeResourcesWrapperElementV110 extends CommonCartridgeElement { - constructor(private readonly props: CommonCartridgeResourcesWrapperElementPropsV110) { - super(props); - } +export type CommonCartridgeResourcesWrapperElementPropsV110 = CommonCartridgeResourcesWrapperElementProps; +export class CommonCartridgeResourcesWrapperElementV110 extends CommonCartridgeResourcesWrapperElement { public getSupportedVersion(): CommonCartridgeVersion { return CommonCartridgeVersion.V_1_1_0; } - - public getManifestXmlObject(): Record { - return { - resources: [ - { - resource: this.props.items.map((items) => items.getManifestXmlObject()), - }, - ], - }; - } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts index 7fe5e1f0cf5..7a5cdae2204 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts @@ -1,4 +1,3 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeMetadataElementPropsV130, createCommonCartridgeOrganizationElementPropsV130, @@ -6,6 +5,7 @@ import { createCommonCartridgeResourcesWrapperElementPropsV130, } from '../../../testing/common-cartridge-element-props.factory'; import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeElementFactoryV130 } from './common-cartridge-element-factory'; import { CommonCartridgeMetadataElementV130 } from './common-cartridge-metadata-element'; import { CommonCartridgeOrganizationElementV130 } from './common-cartridge-organization-element'; @@ -52,9 +52,9 @@ describe('CommonCartridgeElementFactoryV130', () => { const notSupportedProps = createCommonCartridgeOrganizationsWrapperElementPropsV130(); notSupportedProps.type = 'not-supported' as CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; - it('should throw error', () => { + it('should throw ElementTypeNotSupportedLoggableException', () => { expect(() => CommonCartridgeElementFactoryV130.createElement(notSupportedProps)).toThrow( - InternalServerErrorException + ElementTypeNotSupportedLoggableException ); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts index 6a1216e405a..adade3178cb 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts @@ -1,6 +1,6 @@ import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeElement } from '../../interfaces'; -import { createElementTypeNotSupportedError } from '../../utils'; import { CommonCartridgeMetadataElementPropsV130, CommonCartridgeMetadataElementV130, @@ -39,7 +39,7 @@ export class CommonCartridgeElementFactoryV130 { case CommonCartridgeElementType.RESOURCES_WRAPPER: return new CommonCartridgeResourcesWrapperElementV130(props); default: - throw createElementTypeNotSupportedError(type); + throw new ElementTypeNotSupportedLoggableException(type); } } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts index 698b3efea04..e672a08d3d2 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts @@ -1,6 +1,6 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeMetadataElementPropsV130 } from '../../../testing/common-cartridge-element-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeMetadataElementV130 } from './common-cartridge-metadata-element'; describe('CommonCartridgeMetadataElementV130', () => { @@ -27,13 +27,15 @@ describe('CommonCartridgeMetadataElementV130', () => { notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; it('should throw error', () => { - expect(() => new CommonCartridgeMetadataElementV130(notSupportedProps)).toThrow(InternalServerErrorException); + expect(() => new CommonCartridgeMetadataElementV130(notSupportedProps)).toThrow( + VersionNotSupportedLoggableException + ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using common cartridge version 1.3', () => { + describe('when creating metadata xml object', () => { const setup = () => { const props = createCommonCartridgeMetadataElementPropsV130(); const sut = new CommonCartridgeMetadataElementV130(props); @@ -41,10 +43,10 @@ describe('CommonCartridgeMetadataElementV130', () => { return { sut, props }; }; - it('should return correct manifest xml object', () => { + it('should return metadata manifest fragment', () => { const { sut, props } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.METADATA); expect(result).toStrictEqual({ schema: 'IMS Common Cartridge', @@ -67,5 +69,22 @@ describe('CommonCartridgeMetadataElementV130', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const props = createCommonCartridgeMetadataElementPropsV130(); + const sut = new CommonCartridgeMetadataElementV130(props); + + return { sut, props }; + }; + + it('should throw error', () => { + const { sut } = setup(); + + expect(() => sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION)).toThrow( + ElementTypeNotSupportedLoggableException + ); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts index a6477783e57..f7503c779dc 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts @@ -1,5 +1,6 @@ import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement } from '../../interfaces'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, XmlObject } from '../../interfaces'; export type CommonCartridgeMetadataElementPropsV130 = { type: CommonCartridgeElementType.METADATA; @@ -18,7 +19,16 @@ export class CommonCartridgeMetadataElementV130 extends CommonCartridgeElement { return CommonCartridgeVersion.V_1_3_0; } - public getManifestXmlObject(): Record { + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.METADATA: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } + } + + private getManifestXmlObjectInternal(): XmlObject { return { schema: 'IMS Common Cartridge', schemaversion: '1.3.0', diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts index 73cb9b1af5a..6ada585495b 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts @@ -1,7 +1,7 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeOrganizationElementPropsV130 } from '../../../testing/common-cartridge-element-props.factory'; import { createCommonCartridgeWeblinkResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; import { CommonCartridgeOrganizationElementV130 } from './common-cartridge-organization-element'; @@ -31,25 +31,20 @@ describe('CommonCartridgeOrganizationElementV130', () => { it('should throw error', () => { expect(() => new CommonCartridgeOrganizationElementV130(notSupportedProps)).toThrow( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using common cartridge version 1.3.0', () => { + describe('when creating organization xml object', () => { const setup = () => { const resourceProps = createCommonCartridgeWeblinkResourcePropsV130(); - - const subOrganization1Props = createCommonCartridgeOrganizationElementPropsV130( - CommonCartridgeResourceFactory.createResource(resourceProps) - ); - + const subOrganization1Props = createCommonCartridgeOrganizationElementPropsV130(); const subOrganization2Props = createCommonCartridgeOrganizationElementPropsV130([ CommonCartridgeResourceFactory.createResource(resourceProps), ]); - const organizationProps = createCommonCartridgeOrganizationElementPropsV130([ CommonCartridgeElementFactory.createElement(subOrganization1Props), CommonCartridgeElementFactory.createElement(subOrganization2Props), @@ -60,10 +55,10 @@ describe('CommonCartridgeOrganizationElementV130', () => { return { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps }; }; - it('should return correct manifest xml object', () => { + it('should return organization manifest fragment', () => { const { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); expect(result).toStrictEqual({ $: { @@ -74,9 +69,9 @@ describe('CommonCartridgeOrganizationElementV130', () => { { $: { identifier: subOrganization1Props.identifier, - identifierref: resourceProps.identifier, }, title: subOrganization1Props.title, + item: [], }, { $: { @@ -97,5 +92,21 @@ describe('CommonCartridgeOrganizationElementV130', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeOrganizationElementPropsV130(); + const sut = new CommonCartridgeOrganizationElementV130(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts index 14696edfd95..9c71e8b3e29 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts @@ -1,13 +1,13 @@ import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; -import { createIdentifier } from '../../utils'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, CommonCartridgeResource, XmlObject } from '../../interfaces'; export type CommonCartridgeOrganizationElementPropsV130 = { type: CommonCartridgeElementType.ORGANIZATION; version: CommonCartridgeVersion; identifier: string; title: string; - items: CommonCartridgeResource | Array; + items: CommonCartridgeResource | Array; }; export class CommonCartridgeOrganizationElementV130 extends CommonCartridgeElement { @@ -19,35 +19,32 @@ export class CommonCartridgeOrganizationElementV130 extends CommonCartridgeEleme return CommonCartridgeVersion.V_1_3_0; } - public getManifestXmlObject(): Record { - if (this.props.items instanceof CommonCartridgeResource) { - return { - $: { - identifier: this.identifier, - identifierref: this.props.items.identifier, - }, - title: this.title, - }; + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.ORGANIZATION: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); } + } + + public getManifestXmlObjectInternal(): XmlObject { + const xmlObject = Array.isArray(this.props.items) + ? this.getManifestXmlObjectForCollection(this.props.items) + : this.props.items.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); - return { + return xmlObject; + } + + private getManifestXmlObjectForCollection(items: Array): XmlObject { + const xmlObject = { $: { identifier: this.identifier, }, title: this.title, - item: this.props.items.map((item) => { - if (item instanceof CommonCartridgeResource) { - return { - $: { - identifier: createIdentifier(), - identifierref: item.identifier, - }, - title: item.title, - }; - } - - return item.getManifestXmlObject(); - }), + item: items.map((item) => item.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION)), }; + + return xmlObject; } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts index 47c68046764..09cccd60b72 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts @@ -1,9 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeOrganizationElementPropsV130, createCommonCartridgeOrganizationsWrapperElementPropsV130, } from '../../../testing/common-cartridge-element-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; import { CommonCartridgeOrganizationsWrapperElementV130 } from './common-cartridge-organizations-wrapper-element'; @@ -32,14 +32,14 @@ describe('CommonCartridgeOrganizationsWrapperElementV130', () => { it('should throw error', () => { expect(() => new CommonCartridgeOrganizationsWrapperElementV130(notSupportedProps)).toThrow( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using common cartridge version 1.3.0', () => { + describe('when creating organizations wrapper xml object', () => { const setup = () => { const organizationProps = createCommonCartridgeOrganizationElementPropsV130(); const props = createCommonCartridgeOrganizationsWrapperElementPropsV130([ @@ -50,10 +50,10 @@ describe('CommonCartridgeOrganizationsWrapperElementV130', () => { return { sut, organizationProps }; }; - it('should return correct manifest xml object', () => { + it('should return organizations wrapper manifest fragment', () => { const { sut, organizationProps } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATIONS_WRAPPER); expect(result).toStrictEqual({ organization: [ @@ -83,5 +83,21 @@ describe('CommonCartridgeOrganizationsWrapperElementV130', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeOrganizationsWrapperElementPropsV130(); + const sut = new CommonCartridgeOrganizationsWrapperElementV130(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts index 3c00f4844d2..0bf03cd9efc 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts @@ -1,39 +1,13 @@ -import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement } from '../../interfaces'; +import { + CommonCartridgeOrganizationsWrapperElement, + CommonCartridgeOrganizationsWrapperElementProps, +} from '../abstract/common-cartridge-organizations-wrapper-element'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; -export type CommonCartridgeOrganizationsWrapperElementPropsV130 = { - type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; - version: CommonCartridgeVersion; - items: CommonCartridgeElement[]; -}; - -export class CommonCartridgeOrganizationsWrapperElementV130 extends CommonCartridgeElement { - constructor(private readonly props: CommonCartridgeOrganizationsWrapperElementPropsV130) { - super(props); - } +export type CommonCartridgeOrganizationsWrapperElementPropsV130 = CommonCartridgeOrganizationsWrapperElementProps; +export class CommonCartridgeOrganizationsWrapperElementV130 extends CommonCartridgeOrganizationsWrapperElement { public getSupportedVersion(): CommonCartridgeVersion { return CommonCartridgeVersion.V_1_3_0; } - - public getManifestXmlObject(): Record { - return { - organization: [ - { - $: { - identifier: 'org-1', - structure: 'rooted-hierarchy', - }, - item: [ - { - $: { - identifier: 'LearningModules', - }, - item: this.props.items.map((items) => items.getManifestXmlObject()), - }, - ], - }, - ], - }; - } } diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts index 2d71adcc144..17cdf2bac31 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts @@ -1,7 +1,7 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeResourcesWrapperElementPropsV130 } from '../../../testing/common-cartridge-element-props.factory'; import { createCommonCartridgeWeblinkResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; import { CommonCartridgeResourcesWrapperElementV130 } from './common-cartridge-resources-wrapper-element'; @@ -30,14 +30,14 @@ describe('CommonCartridgeResourcesWrapperElementV130', () => { it('should throw error', () => { expect(() => new CommonCartridgeResourcesWrapperElementV130(notSupportedProps)).toThrowError( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using common cartridge version 1.3.0', () => { + describe('when creating resources wrapper xml object', () => { const setup = () => { const weblinkResourceProps = createCommonCartridgeWeblinkResourcePropsV130(); const props = createCommonCartridgeResourcesWrapperElementPropsV130([ @@ -51,7 +51,7 @@ describe('CommonCartridgeResourcesWrapperElementV130', () => { it('should return correct manifest xml object', () => { const { sut, weblinkResourceProps } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.RESOURCES_WRAPPER); expect(result).toStrictEqual({ resources: [ @@ -74,5 +74,23 @@ describe('CommonCartridgeResourcesWrapperElementV130', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeResourcesWrapperElementPropsV130(); + const sut = new CommonCartridgeResourcesWrapperElementV130(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrowError( + ElementTypeNotSupportedLoggableException + ); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts index aa11d0f457a..b9726dd3c63 100644 --- a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts @@ -1,28 +1,13 @@ -import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeElement } from '../../interfaces'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { + CommonCartridgeResourcesWrapperElement, + CommonCartridgeResourcesWrapperElementProps, +} from '../abstract/common-cartridge-resources-wrapper-element'; -export type CommonCartridgeResourcesWrapperElementPropsV130 = { - type: CommonCartridgeElementType.RESOURCES_WRAPPER; - version: CommonCartridgeVersion; - items: CommonCartridgeElement[]; -}; - -export class CommonCartridgeResourcesWrapperElementV130 extends CommonCartridgeElement { - constructor(private readonly props: CommonCartridgeResourcesWrapperElementPropsV130) { - super(props); - } +export type CommonCartridgeResourcesWrapperElementPropsV130 = CommonCartridgeResourcesWrapperElementProps; +export class CommonCartridgeResourcesWrapperElementV130 extends CommonCartridgeResourcesWrapperElement { public getSupportedVersion(): CommonCartridgeVersion { return CommonCartridgeVersion.V_1_3_0; } - - public getManifestXmlObject(): Record { - return { - resources: [ - { - resource: this.props.items.map((items) => items.getManifestXmlObject()), - }, - ], - }; - } } diff --git a/apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.spec.ts b/apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.spec.ts new file mode 100644 index 00000000000..65cd0f664d9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.spec.ts @@ -0,0 +1,23 @@ +import { ElementTypeNotSupportedLoggableException } from './element-type-not-supported.loggable-exception'; +import { CommonCartridgeErrorEnum } from './error.enums'; + +describe('ElementTypeNotSupportedLoggableException', () => { + describe('getLogMessage', () => { + describe('when getting log message', () => { + const exception = new ElementTypeNotSupportedLoggableException('notSupportedType'); + + it('should return log message', () => { + const result = exception.getLogMessage(); + + expect(result).toStrictEqual({ + type: CommonCartridgeErrorEnum.ELEMENT_TYPE_NOT_SUPPORTED, + stack: exception.stack, + data: { + type: 'notSupportedType', + message: 'Common Cartridge element type notSupportedType is not supported', + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.ts b/apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.ts new file mode 100644 index 00000000000..19dd0ff941d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/element-type-not-supported.loggable-exception.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; +import { CommonCartridgeErrorEnum } from './error.enums'; + +export class ElementTypeNotSupportedLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly type: string) { + super({ + type: CommonCartridgeErrorEnum.ELEMENT_TYPE_NOT_SUPPORTED, + }); + } + + public getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: CommonCartridgeErrorEnum.ELEMENT_TYPE_NOT_SUPPORTED, + stack: this.stack, + data: { + type: this.type, + message: `Common Cartridge element type ${this.type} is not supported`, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/errors/error.enums.ts b/apps/server/src/modules/common-cartridge/export/errors/error.enums.ts new file mode 100644 index 00000000000..0f5081717c9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/error.enums.ts @@ -0,0 +1,7 @@ +export enum CommonCartridgeErrorEnum { + VERSION_NOT_SUPPORTED = 'VERSION_NOT_SUPPORTED', + RESOURCE_TYPE_NOT_SUPPORTED = 'RESOURCE_TYPE_NOT_SUPPORTED', + ELEMENT_TYPE_NOT_SUPPORTED = 'ELEMENT_TYPE_NOT_SUPPORTED', + INTENDED_USE_NOT_SUPPORTED = 'INTENDED_USE_NOT_SUPPORTED', + MISSING_METADATA = 'MISSING_METADATA', +} diff --git a/apps/server/src/modules/common-cartridge/export/errors/index.ts b/apps/server/src/modules/common-cartridge/export/errors/index.ts new file mode 100644 index 00000000000..a3f364f2793 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/index.ts @@ -0,0 +1,5 @@ +export { ElementTypeNotSupportedLoggableException } from './element-type-not-supported.loggable-exception'; +export { IntendedUseNotSupportedLoggableException } from './intended-use-not-supported.loggable-exception'; +export { MissingMetadataLoggableException } from './missing-metadata.loggable-exception'; +export { ResourceTypeNotSupportedLoggableException } from './resource-type-not-supported.loggable-exception'; +export { VersionNotSupportedLoggableException } from './version-not-supported.loggable-exception'; diff --git a/apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.spec.ts b/apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.spec.ts new file mode 100644 index 00000000000..3bc2353cdae --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.spec.ts @@ -0,0 +1,23 @@ +import { CommonCartridgeErrorEnum } from './error.enums'; +import { IntendedUseNotSupportedLoggableException } from './intended-use-not-supported.loggable-exception'; + +describe('IntendedUseNotSupportedLoggableException', () => { + describe('getLogMessage', () => { + describe('when getting log message', () => { + const exception = new IntendedUseNotSupportedLoggableException('notSupportedIntendedUse'); + + it('should return log message', () => { + const result = exception.getLogMessage(); + + expect(result).toStrictEqual({ + type: CommonCartridgeErrorEnum.INTENDED_USE_NOT_SUPPORTED, + stack: exception.stack, + data: { + intendedUse: 'notSupportedIntendedUse', + message: 'Common Cartridge intended use notSupportedIntendedUse is not supported', + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.ts b/apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.ts new file mode 100644 index 00000000000..5259bd517f1 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/intended-use-not-supported.loggable-exception.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; +import { CommonCartridgeErrorEnum } from './error.enums'; + +export class IntendedUseNotSupportedLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly intendedUse: string) { + super({ + type: CommonCartridgeErrorEnum.INTENDED_USE_NOT_SUPPORTED, + }); + } + + public getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: CommonCartridgeErrorEnum.INTENDED_USE_NOT_SUPPORTED, + stack: this.stack, + data: { + intendedUse: this.intendedUse, + message: `Common Cartridge intended use ${this.intendedUse} is not supported`, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.spec.ts b/apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.spec.ts new file mode 100644 index 00000000000..3d0b32d87d9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.spec.ts @@ -0,0 +1,22 @@ +import { CommonCartridgeErrorEnum } from './error.enums'; +import { MissingMetadataLoggableException } from './missing-metadata.loggable-exception'; + +describe('MissingMetadataLoggableException', () => { + describe('getLogMessage', () => { + describe('when getting log message', () => { + const exception = new MissingMetadataLoggableException(); + + it('should return log message', () => { + const result = exception.getLogMessage(); + + expect(result).toStrictEqual({ + type: CommonCartridgeErrorEnum.MISSING_METADATA, + stack: exception.stack, + data: { + message: 'Metadata is required', + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.ts b/apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.ts new file mode 100644 index 00000000000..9ee3d6bcff0 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/missing-metadata.loggable-exception.ts @@ -0,0 +1,23 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; +import { CommonCartridgeErrorEnum } from './error.enums'; + +export class MissingMetadataLoggableException extends InternalServerErrorException implements Loggable { + constructor() { + super({ + type: CommonCartridgeErrorEnum.MISSING_METADATA, + }); + } + + public getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: CommonCartridgeErrorEnum.MISSING_METADATA, + stack: this.stack, + data: { + message: 'Metadata is required', + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.spec.ts b/apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.spec.ts new file mode 100644 index 00000000000..b51cb43cd0b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.spec.ts @@ -0,0 +1,23 @@ +import { CommonCartridgeErrorEnum } from './error.enums'; +import { ResourceTypeNotSupportedLoggableException } from './resource-type-not-supported.loggable-exception'; + +describe('ResourceTypeNotSupportedLoggableException', () => { + describe('getLogMessage', () => { + describe('when getting log message', () => { + const exception = new ResourceTypeNotSupportedLoggableException('notSupportedType'); + + it('should return log message', () => { + const result = exception.getLogMessage(); + + expect(result).toStrictEqual({ + type: CommonCartridgeErrorEnum.RESOURCE_TYPE_NOT_SUPPORTED, + stack: exception.stack, + data: { + type: 'notSupportedType', + message: 'Common Cartridge resource type notSupportedType is not supported', + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.ts b/apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.ts new file mode 100644 index 00000000000..4d9d9d8a8e1 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/resource-type-not-supported.loggable-exception.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; +import { CommonCartridgeErrorEnum } from './error.enums'; + +export class ResourceTypeNotSupportedLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly type: string) { + super({ + type: CommonCartridgeErrorEnum.RESOURCE_TYPE_NOT_SUPPORTED, + }); + } + + public getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: CommonCartridgeErrorEnum.RESOURCE_TYPE_NOT_SUPPORTED, + stack: this.stack, + data: { + type: this.type, + message: `Common Cartridge resource type ${this.type} is not supported`, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.spec.ts b/apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.spec.ts new file mode 100644 index 00000000000..5ec29c55fdb --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.spec.ts @@ -0,0 +1,23 @@ +import { CommonCartridgeErrorEnum } from './error.enums'; +import { VersionNotSupportedLoggableException } from './version-not-supported.loggable-exception'; + +describe('VersionNotSupportedLoggableException', () => { + describe('getLogMessage', () => { + describe('when getting log message', () => { + const exception = new VersionNotSupportedLoggableException('notSupportedVersion'); + + it('should return log message', () => { + const result = exception.getLogMessage(); + + expect(result).toStrictEqual({ + type: CommonCartridgeErrorEnum.VERSION_NOT_SUPPORTED, + stack: exception.stack, + data: { + version: 'notSupportedVersion', + message: 'Common Cartridge version notSupportedVersion is not supported', + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.ts b/apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.ts new file mode 100644 index 00000000000..bb22519217e --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/errors/version-not-supported.loggable-exception.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; +import { CommonCartridgeErrorEnum } from './error.enums'; + +export class VersionNotSupportedLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly version: string) { + super({ + type: CommonCartridgeErrorEnum.VERSION_NOT_SUPPORTED, + }); + } + + public getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: CommonCartridgeErrorEnum.VERSION_NOT_SUPPORTED, + stack: this.stack, + data: { + version: this.version, + message: `Common Cartridge version ${this.version} is not supported`, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-base.interface.ts b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-base.interface.ts new file mode 100644 index 00000000000..0b768edfbc7 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-base.interface.ts @@ -0,0 +1,30 @@ +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { VersionNotSupportedLoggableException } from '../errors'; + +type CommonCartridgeBaseProps = { + version: CommonCartridgeVersion; + identifier?: string; + title?: string; +}; + +export abstract class CommonCartridgeBase { + protected constructor(public readonly baseProps: CommonCartridgeBaseProps) { + this.checkVersion(baseProps.version); + } + + public get identifier(): string | undefined { + return this.baseProps.identifier; + } + + public get title(): string | undefined { + return this.baseProps.title; + } + + abstract getSupportedVersion(): CommonCartridgeVersion; + + private checkVersion(target: CommonCartridgeVersion): void { + if (this.getSupportedVersion() !== target) { + throw new VersionNotSupportedLoggableException(target); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts index 869aa6f576f..dae8a1ecf78 100644 --- a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts +++ b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts @@ -1,43 +1,14 @@ -import { CommonCartridgeVersion } from '../common-cartridge.enums'; -import { createVersionNotSupportedError } from '../utils'; - -type CommonCartridgeElementProps = { - version: CommonCartridgeVersion; - identifier?: string; - title?: string; -}; +import { CommonCartridgeElementType } from '../common-cartridge.enums'; +import { CommonCartridgeBase } from './common-cartridge-base.interface'; +import { XmlObject } from './xml-object.interface'; /** * Every element which should be listed in the Common Cartridge manifest must implement this interface. */ -export abstract class CommonCartridgeElement { - protected constructor(private readonly baseProps: CommonCartridgeElementProps) { - this.checkVersion(baseProps.version); - } - - public get identifier(): string | undefined { - return this.baseProps.identifier; - } - - public get title(): string | undefined { - return this.baseProps.title; - } - - /** - * Every element must know which versions it supports. - * @returns The supported versions for this element. - */ - abstract getSupportedVersion(): CommonCartridgeVersion; - +export abstract class CommonCartridgeElement extends CommonCartridgeBase { /** * This method is used to build the imsmanifest.xml file. * @returns The XML object representation for the imsmanifest.xml file. */ - abstract getManifestXmlObject(): Record; - - private checkVersion(target: CommonCartridgeVersion): void { - if (this.getSupportedVersion() !== target) { - throw createVersionNotSupportedError(target); - } - } + abstract getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject; } diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts index dfa3adbc8b4..b75b3524d06 100644 --- a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts +++ b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts @@ -4,12 +4,6 @@ import { CommonCartridgeElement } from './common-cartridge-element.interface'; * Every resource which should be added to the Common Cartridge archive must implement this interface. */ export abstract class CommonCartridgeResource extends CommonCartridgeElement { - /** - * In later Common Cartridge versions, resources can be inlined in the imsmanifest.xml file. - * @returns true if the resource can be inlined, otherwise false. - */ - abstract canInline(): boolean; - /** * This method is used to determine the path of the resource in the Common Cartridge archive. * @returns The path of the resource in the Common Cartridge archive. diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/index.ts b/apps/server/src/modules/common-cartridge/export/interfaces/index.ts index aaefb39018c..1f5d4394c4d 100644 --- a/apps/server/src/modules/common-cartridge/export/interfaces/index.ts +++ b/apps/server/src/modules/common-cartridge/export/interfaces/index.ts @@ -1,2 +1,4 @@ +export { CommonCartridgeBase } from './common-cartridge-base.interface'; export { CommonCartridgeElement } from './common-cartridge-element.interface'; export { CommonCartridgeResource } from './common-cartridge-resource.interface'; +export { XmlObject } from './xml-object.interface'; diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/xml-object.interface.ts b/apps/server/src/modules/common-cartridge/export/interfaces/xml-object.interface.ts new file mode 100644 index 00000000000..583e52d208e --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/interfaces/xml-object.interface.ts @@ -0,0 +1 @@ +export interface XmlObject extends Record {} diff --git a/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts index 9bd274d4810..d1d6ab6ae76 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts @@ -1,9 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeWebContentResourcePropsV110, createCommonCartridgeWebContentResourcePropsV130, } from '../../testing/common-cartridge-resource-props.factory'; import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { VersionNotSupportedLoggableException } from '../errors'; import { CommonCartridgeResourceFactory } from './common-cartridge-resource-factory'; import { CommonCartridgeWebContentResourcePropsV110, CommonCartridgeWebContentResourceV110 } from './v1.1.0'; import { CommonCartridgeWebContentResourceV130 } from './v1.3.0'; @@ -35,14 +35,14 @@ describe('CommonCartridgeResourceVersion', () => { CommonCartridgeVersion.V_1_4_0, ]; - it('should throw InternalServerErrorException', () => { + it('should throw VersionNotSupportedLoggableException', () => { notSupportedVersions.forEach((version) => { expect(() => CommonCartridgeResourceFactory.createResource({ version, type: CommonCartridgeResourceType.WEB_CONTENT, } as CommonCartridgeWebContentResourcePropsV110) - ).toThrow(InternalServerErrorException); + ).toThrow(VersionNotSupportedLoggableException); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts index 673672c129e..1ef3cc1428d 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts @@ -1,6 +1,7 @@ import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { VersionNotSupportedLoggableException } from '../errors'; import { CommonCartridgeResource } from '../interfaces'; -import { OmitVersionAndFolder, createVersionNotSupportedError } from '../utils'; +import { OmitVersionAndFolder } from '../utils'; import { CommonCartridgeManifestResourcePropsV110, CommonCartridgeResourceFactoryV110, @@ -20,7 +21,7 @@ export type CommonCartridgeResourceProps = | OmitVersionAndFolder | OmitVersionAndFolder; -type CommonCartridgeResourcePropsInternal = +export type CommonCartridgeResourcePropsInternal = | CommonCartridgeManifestResourcePropsV110 | CommonCartridgeWebContentResourcePropsV110 | CommonCartridgeWebLinkResourcePropsV110 @@ -38,7 +39,7 @@ export class CommonCartridgeResourceFactory { case CommonCartridgeVersion.V_1_3_0: return CommonCartridgeResourceFactoryV130.createResource(props); default: - throw createVersionNotSupportedError(version); + throw new VersionNotSupportedLoggableException(version); } } } diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts index aa63bb9237f..129738f323f 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts @@ -8,27 +8,14 @@ import { CommonCartridgeVersion, } from '../../common-cartridge.enums'; import { CommonCartridgeElementFactory } from '../../elements/common-cartridge-element-factory'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import * as utils from '../../utils'; import { CommonCartridgeResourceFactory } from '../common-cartridge-resource-factory'; import { CommonCartridgeManifestResourceV110 } from './common-cartridge-manifest-resource'; describe('CommonCartridgeManifestResourceV110', () => { - describe('canInline', () => { - describe('when using Common Cartridge version 1.1.0', () => { - const setup = () => { - const props = createCommonCartridgeManifestResourcePropsV110(); - const sut = new CommonCartridgeManifestResourceV110(props); - - return { sut }; - }; - - it('should return false', () => { - const { sut } = setup(); - - const result = sut.canInline(); - - expect(result).toBe(false); - }); - }); + beforeEach(() => { + jest.clearAllMocks(); }); describe('getFilePath', () => { @@ -100,6 +87,11 @@ describe('CommonCartridgeManifestResourceV110', () => { resources: [resource1, resource2], }); + // we need this, otherwise the identifier will be random and we have to updated + // the manifest.xml file which we will compare with the expected content in the test + const mockValues = ['o1', 'o2']; + jest.spyOn(utils, 'createIdentifier').mockImplementation(() => mockValues.shift() ?? ''); + return { sut }; }; @@ -144,4 +136,46 @@ describe('CommonCartridgeManifestResourceV110', () => { }); }); }); + + describe('getManifestXmlObject', () => { + describe('when creating manifest xml object', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV110(); + const sut = new CommonCartridgeManifestResourceV110(props); + + return { sut }; + }; + + it('should return manifest xml object', () => { + const { sut } = setup(); + + const result = sut.getManifestXmlObject(CommonCartridgeElementType.MANIFEST); + + expect(result).toStrictEqual({ + manifest: { + $: expect.any(Object) as unknown, + metadata: expect.any(Object) as unknown, + organizations: expect.any(Object) as unknown, + resources: expect.any(Object) as unknown, + }, + }); + }); + }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeManifestResourcePropsV110(); + const sut = new CommonCartridgeManifestResourceV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); + }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts index ae578c34e4a..1f597492d03 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts @@ -4,7 +4,8 @@ import { CommonCartridgeVersion, } from '../../common-cartridge.enums'; import { CommonCartridgeElementFactory } from '../../elements/common-cartridge-element-factory'; -import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, CommonCartridgeResource, XmlObject } from '../../interfaces'; import { buildXmlString } from '../../utils'; export type CommonCartridgeManifestResourcePropsV110 = { @@ -21,8 +22,17 @@ export class CommonCartridgeManifestResourceV110 extends CommonCartridgeResource super(props); } - public canInline(): boolean { - return false; + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.MANIFEST: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } } public getFilePath(): string { @@ -30,14 +40,10 @@ export class CommonCartridgeManifestResourceV110 extends CommonCartridgeResource } public getFileContent(): string { - return buildXmlString(this.getManifestXmlObject()); - } - - public getSupportedVersion(): CommonCartridgeVersion { - return CommonCartridgeVersion.V_1_1_0; + return buildXmlString(this.getManifestXmlObjectInternal()); } - public getManifestXmlObject(): Record { + private getManifestXmlObjectInternal(): XmlObject { return { manifest: { $: { @@ -51,17 +57,17 @@ export class CommonCartridgeManifestResourceV110 extends CommonCartridgeResource 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest https://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd ' + 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource https://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd', }, - metadata: this.props.metadata.getManifestXmlObject(), + metadata: this.props.metadata.getManifestXmlObject(CommonCartridgeElementType.METADATA), organizations: CommonCartridgeElementFactory.createElement({ type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER, version: this.props.version, items: this.props.organizations, - }).getManifestXmlObject(), + }).getManifestXmlObject(CommonCartridgeElementType.ORGANIZATIONS_WRAPPER), ...CommonCartridgeElementFactory.createElement({ type: CommonCartridgeElementType.RESOURCES_WRAPPER, version: this.props.version, items: this.props.resources, - }).getManifestXmlObject(), + }).getManifestXmlObject(CommonCartridgeElementType.RESOURCES_WRAPPER), }, }; } diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts index fd93be44d2d..3e214a828ee 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts @@ -1,9 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeManifestResourcePropsV110, createCommonCartridgeWebContentResourcePropsV110, createCommonCartridgeWeblinkResourcePropsV110, } from '../../../testing/common-cartridge-resource-props.factory'; +import { ResourceTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeManifestResourceV110 } from './common-cartridge-manifest-resource'; import { CommonCartridgeResourceFactoryV110 } from './common-cartridge-resource-factory'; import { CommonCartridgeWebContentResourceV110 } from './common-cartridge-web-content-resource'; @@ -41,10 +41,10 @@ describe('CommonCartridgeResourceFactoryV110', () => { }); describe('when resource type is not supported', () => { - it('should throw error', () => { + it('should throw ResourceTypeNotSupportedLoggableException', () => { expect(() => CommonCartridgeResourceFactoryV110.createResource({} as CommonCartridgeWebLinkResourcePropsV110) - ).toThrow(InternalServerErrorException); + ).toThrow(ResourceTypeNotSupportedLoggableException); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts index 4e13ec77587..ffb94273dce 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts @@ -1,6 +1,6 @@ import { CommonCartridgeResourceType } from '../../common-cartridge.enums'; +import { ResourceTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeResource } from '../../interfaces'; -import { createResourceTypeNotSupportedError } from '../../utils'; import { CommonCartridgeManifestResourcePropsV110, CommonCartridgeManifestResourceV110, @@ -31,7 +31,7 @@ export class CommonCartridgeResourceFactoryV110 { case CommonCartridgeResourceType.WEB_LINK: return new CommonCartridgeWebLinkResourceV110(props); default: - throw createResourceTypeNotSupportedError(type); + throw new ResourceTypeNotSupportedLoggableException(type); } } } diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts index 169986b451e..40ae8a4f2e1 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts @@ -1,28 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeWebContentResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeWebContentResourceV110 } from './common-cartridge-web-content-resource'; describe('CommonCartridgeWebContentResourceV110', () => { - describe('canInline', () => { - describe('when using Common Cartridge version 1.1.0', () => { - const setup = () => { - const props = createCommonCartridgeWebContentResourcePropsV110(); - const sut = new CommonCartridgeWebContentResourceV110(props); - - return { sut }; - }; - - it('should return false', () => { - const { sut } = setup(); - - const result = sut.canInline(); - - expect(result).toBe(false); - }); - }); - }); - describe('getFilePath', () => { describe('when using Common Cartridge version 1.1.0', () => { const setup = () => { @@ -85,14 +66,37 @@ describe('CommonCartridgeWebContentResourceV110', () => { it('should throw error', () => { expect(() => new CommonCartridgeWebContentResourceV110(notSupportedProps)).toThrow( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.1.0', () => { + describe('when creating organization xml object', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut, props }; + }; + + it('should return organization manifest fragment', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); + + expect(result).toEqual({ + $: { + identifier: expect.any(String), + identifierref: props.identifier, + }, + title: props.title, + }); + }); + }); + + describe('when creating resource xml object', () => { const setup = () => { const props = createCommonCartridgeWebContentResourcePropsV110(); const sut = new CommonCartridgeWebContentResourceV110(props); @@ -100,10 +104,10 @@ describe('CommonCartridgeWebContentResourceV110', () => { return { sut, props }; }; - it('should return the correct XML object', () => { + it('should return resource manifest fragment', () => { const { sut, props } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.RESOURCE); expect(result).toEqual({ $: { @@ -119,5 +123,21 @@ describe('CommonCartridgeWebContentResourceV110', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts index f684709e246..e097a707960 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts @@ -1,10 +1,13 @@ import { + CommonCartridgeElementType, CommonCartridgeIntendedUseType, CommonCartridgeResourceType, CommonCartridgeVersion, } from '../../common-cartridge.enums'; -import { CommonCartridgeResource } from '../../interfaces'; -import { checkIntendedUse } from '../../utils'; +import { CommonCartridgeGuard } from '../../common-cartridge.guard'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeResource, XmlObject } from '../../interfaces'; +import { createIdentifier } from '../../utils'; export type CommonCartridgeWebContentResourcePropsV110 = { type: CommonCartridgeResourceType.WEB_CONTENT; @@ -25,11 +28,25 @@ export class CommonCartridgeWebContentResourceV110 extends CommonCartridgeResour constructor(private readonly props: CommonCartridgeWebContentResourcePropsV110) { super(props); - checkIntendedUse(props.intendedUse, CommonCartridgeWebContentResourceV110.SUPPORTED_INTENDED_USES); + CommonCartridgeGuard.checkIntendedUse( + props.intendedUse, + CommonCartridgeWebContentResourceV110.SUPPORTED_INTENDED_USES + ); } - public canInline(): boolean { - return false; + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.RESOURCE: + return this.getManifestResourceXmlObject(); + case CommonCartridgeElementType.ORGANIZATION: + return this.getManifestOrganizationXmlObject(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } } public getFilePath(): string { @@ -40,11 +57,17 @@ export class CommonCartridgeWebContentResourceV110 extends CommonCartridgeResour return this.props.html; } - public getSupportedVersion(): CommonCartridgeVersion { - return CommonCartridgeVersion.V_1_1_0; + private getManifestOrganizationXmlObject(): XmlObject { + return { + $: { + identifier: createIdentifier(), + identifierref: this.props.identifier, + }, + title: this.props.title, + }; } - public getManifestXmlObject(): Record { + private getManifestResourceXmlObject(): XmlObject { return { $: { identifier: this.props.identifier, diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts index 79881d285f4..b697a51f871 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts @@ -1,29 +1,11 @@ import { InternalServerErrorException } from '@nestjs/common'; import { readFile } from 'fs/promises'; import { createCommonCartridgeWeblinkResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeWebLinkResourceV110 } from './common-cartridge-web-link-resource'; describe('CommonCartridgeWebLinkResourceV110', () => { - describe('canInline', () => { - describe('when using Common Cartridge version 1.3.0', () => { - const setup = () => { - const props = createCommonCartridgeWeblinkResourcePropsV110(); - const sut = new CommonCartridgeWebLinkResourceV110(props); - - return { sut }; - }; - - it('should return false', () => { - const { sut } = setup(); - - const result = sut.canInline(); - - expect(result).toBe(false); - }); - }); - }); - describe('getFilePath', () => { describe('when using Common Cartridge version 1.1.0', () => { const setup = () => { @@ -99,7 +81,7 @@ describe('CommonCartridgeWebLinkResourceV110', () => { }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.1.0', () => { + describe('when creating organization xml object', () => { const setup = () => { const props = createCommonCartridgeWeblinkResourcePropsV110(); const sut = new CommonCartridgeWebLinkResourceV110(props); @@ -107,10 +89,33 @@ describe('CommonCartridgeWebLinkResourceV110', () => { return { sut, props }; }; - it('should return the manifest XML object', () => { + it('should return resource manifest fragment', () => { const { sut, props } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); + + expect(result).toEqual({ + $: { + identifier: expect.any(String), + identifierref: props.identifier, + }, + title: props.title, + }); + }); + }); + + describe('when creating resource xml object', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut, props }; + }; + + it('should return resource manifest fragment', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(CommonCartridgeElementType.RESOURCE); expect(result).toEqual({ $: { @@ -125,5 +130,21 @@ describe('CommonCartridgeWebLinkResourceV110', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeWeblinkResourcePropsV110(); + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts index 4da6c641215..b6fd594de7d 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts @@ -1,6 +1,11 @@ -import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeResource } from '../../interfaces'; -import { buildXmlString } from '../../utils'; +import { + CommonCartridgeElementType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeResource, XmlObject } from '../../interfaces'; +import { buildXmlString, createIdentifier } from '../../utils'; export type CommonCartridgeWebLinkResourcePropsV110 = { type: CommonCartridgeResourceType.WEB_LINK; @@ -18,8 +23,19 @@ export class CommonCartridgeWebLinkResourceV110 extends CommonCartridgeResource super(props); } - public canInline(): boolean { - return false; + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.RESOURCE: + return this.getManifestResourceXmlObject(); + case CommonCartridgeElementType.ORGANIZATION: + return this.getManifestOrganizationXmlObject(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } } public getFilePath(): string { @@ -47,14 +63,20 @@ export class CommonCartridgeWebLinkResourceV110 extends CommonCartridgeResource }); } - public getSupportedVersion(): CommonCartridgeVersion { - return CommonCartridgeVersion.V_1_1_0; + private getManifestOrganizationXmlObject(): XmlObject { + return { + $: { + identifier: createIdentifier(), + identifierref: this.identifier, + }, + title: this.title, + }; } - public getManifestXmlObject(): Record { + private getManifestResourceXmlObject(): XmlObject { return { $: { - identifier: this.props.identifier, + identifier: this.identifier, type: 'imswl_xmlv1p1', }, file: { diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts index f49caa016c9..eb0bd8e5696 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts @@ -9,27 +9,14 @@ import { } from '../../common-cartridge.enums'; import { CommonCartridgeElementFactory } from '../../elements/common-cartridge-element-factory'; import { CommonCartridgeElementFactoryV130 } from '../../elements/v1.3.0'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import * as utils from '../../utils'; import { CommonCartridgeResourceFactory } from '../common-cartridge-resource-factory'; import { CommonCartridgeManifestResourceV130 } from './common-cartridge-manifest-resource'; describe('CommonCartridgeManifestResourceV130', () => { - describe('canInline', () => { - describe('when using Common Cartridge version 1.3.0', () => { - const setup = () => { - const props = createCommonCartridgeManifestResourcePropsV130(); - const sut = new CommonCartridgeManifestResourceV130(props); - - return { sut }; - }; - - it('should return false', () => { - const { sut } = setup(); - - const result = sut.canInline(); - - expect(result).toBe(false); - }); - }); + beforeEach(() => { + jest.clearAllMocks(); }); describe('getFilePath', () => { @@ -101,6 +88,11 @@ describe('CommonCartridgeManifestResourceV130', () => { resources: [resource1, resource2], }); + // we need this, otherwise the identifier will be random and we have to updated + // the manifest.xml file which we will compare with the expected content in the test + const mockValues = ['o1', 'o2']; + jest.spyOn(utils, 'createIdentifier').mockImplementation(() => mockValues.shift() ?? ''); + return { sut }; }; @@ -145,4 +137,46 @@ describe('CommonCartridgeManifestResourceV130', () => { }); }); }); + + describe('getManifestXmlObject', () => { + describe('when creating manifest xml object', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV130(); + const sut = new CommonCartridgeManifestResourceV130(props); + + return { sut }; + }; + + it('should return manifest xml object', () => { + const { sut } = setup(); + + const result = sut.getManifestXmlObject(CommonCartridgeElementType.MANIFEST); + + expect(result).toStrictEqual({ + manifest: { + $: expect.any(Object) as unknown, + metadata: expect.any(Object) as unknown, + organizations: expect.any(Object) as unknown, + resources: expect.any(Object) as unknown, + }, + }); + }); + }); + + describe('when element type is not supported', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeManifestResourcePropsV130(); + const sut = new CommonCartridgeManifestResourceV130(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); + }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts index 3da27cb30b8..63f543baf07 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts @@ -7,7 +7,8 @@ import { CommonCartridgeOrganizationsWrapperElementV130, CommonCartridgeResourcesWrapperElementV130, } from '../../elements/v1.3.0'; -import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeElement, CommonCartridgeResource, XmlObject } from '../../interfaces'; import { buildXmlString } from '../../utils'; export type CommonCartridgeManifestResourcePropsV130 = { @@ -24,23 +25,28 @@ export class CommonCartridgeManifestResourceV130 extends CommonCartridgeResource super(props); } - public canInline(): boolean { - return false; - } - public getFilePath(): string { return 'imsmanifest.xml'; } public getFileContent(): string { - return buildXmlString(this.getManifestXmlObject()); + return buildXmlString(this.getManifestXmlObject(CommonCartridgeElementType.MANIFEST)); } public getSupportedVersion(): CommonCartridgeVersion { return CommonCartridgeVersion.V_1_3_0; } - public getManifestXmlObject(): Record { + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.MANIFEST: + return this.getManifestXmlObjectInternal(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } + } + + public getManifestXmlObjectInternal(): XmlObject { return { manifest: { $: { @@ -54,17 +60,17 @@ export class CommonCartridgeManifestResourceV130 extends CommonCartridgeResource 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest https://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lommanifest_v1p0.xsd ' + 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource https://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lomresource_v1p0.xsd', }, - metadata: this.props.metadata.getManifestXmlObject(), + metadata: this.props.metadata.getManifestXmlObject(CommonCartridgeElementType.METADATA), organizations: new CommonCartridgeOrganizationsWrapperElementV130({ type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER, version: this.props.version, items: this.props.organizations, - }).getManifestXmlObject(), + }).getManifestXmlObject(CommonCartridgeElementType.ORGANIZATIONS_WRAPPER), ...new CommonCartridgeResourcesWrapperElementV130({ type: CommonCartridgeElementType.RESOURCES_WRAPPER, version: this.props.version, items: this.props.resources, - }).getManifestXmlObject(), + }).getManifestXmlObject(CommonCartridgeElementType.RESOURCES_WRAPPER), }, }; } diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts index ad630ea25d8..799e435ee00 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts @@ -1,9 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeManifestResourcePropsV130, createCommonCartridgeWebContentResourcePropsV130, createCommonCartridgeWeblinkResourcePropsV130, } from '../../../testing/common-cartridge-resource-props.factory'; +import { ResourceTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeManifestResourceV130 } from './common-cartridge-manifest-resource'; import { CommonCartridgeResourceFactoryV130 } from './common-cartridge-resource-factory'; import { CommonCartridgeWebContentResourceV130 } from './common-cartridge-web-content-resource'; @@ -41,10 +41,10 @@ describe('CommonCartridgeResourceFactoryV130', () => { }); describe('when resource type is not supported', () => { - it('should throw error', () => { + it('should throw ResourceTypeNotSupportedLoggableException', () => { expect(() => CommonCartridgeResourceFactoryV130.createResource({} as CommonCartridgeWebLinkResourcePropsV130) - ).toThrow(InternalServerErrorException); + ).toThrow(ResourceTypeNotSupportedLoggableException); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts index 9be5fad11c1..a842c1a4a98 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts @@ -1,6 +1,6 @@ import { CommonCartridgeResourceType } from '../../common-cartridge.enums'; +import { ResourceTypeNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeResource } from '../../interfaces'; -import { createResourceTypeNotSupportedError } from '../../utils'; import { CommonCartridgeManifestResourcePropsV130, CommonCartridgeManifestResourceV130, @@ -31,7 +31,7 @@ export class CommonCartridgeResourceFactoryV130 { case CommonCartridgeResourceType.WEB_LINK: return new CommonCartridgeWebLinkResourceV130(props); default: - throw createResourceTypeNotSupportedError(type); + throw new ResourceTypeNotSupportedLoggableException(type); } } } diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts index e1b3334fd7d..44ab6b65002 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts @@ -1,28 +1,9 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { createCommonCartridgeWebContentResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeWebContentResourceV130 } from './common-cartridge-web-content-resource'; describe('CommonCartridgeWebContentResourceV130', () => { - describe('canInline', () => { - describe('when using Common Cartridge version 1.3.0', () => { - const setup = () => { - const props = createCommonCartridgeWebContentResourcePropsV130(); - const sut = new CommonCartridgeWebContentResourceV130(props); - - return { sut }; - }; - - it('should return false', () => { - const { sut } = setup(); - - const result = sut.canInline(); - - expect(result).toBe(false); - }); - }); - }); - describe('getFilePath', () => { describe('when using Common Cartridge version 1.3.0', () => { const setup = () => { @@ -85,14 +66,37 @@ describe('CommonCartridgeWebContentResourceV130', () => { it('should throw error', () => { expect(() => new CommonCartridgeWebContentResourceV130(notSupportedProps)).toThrow( - InternalServerErrorException + VersionNotSupportedLoggableException ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.3.0', () => { + describe('when creating organization xml object', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut, props }; + }; + + it('should return organization manifest fragment', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); + + expect(result).toEqual({ + $: { + identifier: expect.any(String), + identifierref: props.identifier, + }, + title: props.title, + }); + }); + }); + + describe('when creating resource xml object', () => { const setup = () => { const props = createCommonCartridgeWebContentResourcePropsV130(); const sut = new CommonCartridgeWebContentResourceV130(props); @@ -100,10 +104,10 @@ describe('CommonCartridgeWebContentResourceV130', () => { return { sut, props }; }; - it('should return the manifest XML object', () => { + it('should return resource manifest fragment', () => { const { sut, props } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.RESOURCE); expect(result).toEqual({ $: { @@ -119,5 +123,21 @@ describe('CommonCartridgeWebContentResourceV130', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts index eb168087a52..21aa09a0a2d 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts @@ -1,10 +1,13 @@ import { + CommonCartridgeElementType, CommonCartridgeIntendedUseType, CommonCartridgeResourceType, CommonCartridgeVersion, } from '../../common-cartridge.enums'; -import { CommonCartridgeResource } from '../../interfaces'; -import { checkIntendedUse } from '../../utils'; +import { CommonCartridgeGuard } from '../../common-cartridge.guard'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeResource, XmlObject } from '../../interfaces'; +import { createIdentifier } from '../../utils'; export type CommonCartridgeWebContentResourcePropsV130 = { type: CommonCartridgeResourceType.WEB_CONTENT; @@ -26,11 +29,25 @@ export class CommonCartridgeWebContentResourceV130 extends CommonCartridgeResour constructor(private readonly props: CommonCartridgeWebContentResourcePropsV130) { super(props); - checkIntendedUse(props.intendedUse, CommonCartridgeWebContentResourceV130.SUPPORTED_INTENDED_USES); + CommonCartridgeGuard.checkIntendedUse( + props.intendedUse, + CommonCartridgeWebContentResourceV130.SUPPORTED_INTENDED_USES + ); } - public canInline(): boolean { - return false; + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.RESOURCE: + return this.getManifestResourceXmlObject(); + case CommonCartridgeElementType.ORGANIZATION: + return this.getManifestOrganizationXmlObject(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } } public getFilePath(): string { @@ -41,11 +58,17 @@ export class CommonCartridgeWebContentResourceV130 extends CommonCartridgeResour return this.props.html; } - public getSupportedVersion(): CommonCartridgeVersion { - return CommonCartridgeVersion.V_1_3_0; + private getManifestOrganizationXmlObject(): XmlObject { + return { + $: { + identifier: createIdentifier(), + identifierref: this.props.identifier, + }, + title: this.props.title, + }; } - public getManifestXmlObject(): Record { + private getManifestResourceXmlObject(): XmlObject { return { $: { identifier: this.props.identifier, diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts index d6aee5e394f..83985e8f717 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts @@ -1,29 +1,10 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { readFile } from 'node:fs/promises'; import { createCommonCartridgeWeblinkResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; -import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException, VersionNotSupportedLoggableException } from '../../errors'; import { CommonCartridgeWebLinkResourceV130 } from './common-cartridge-web-link-resource'; describe('CommonCartridgeWebLinkResourceV130', () => { - describe('canInline', () => { - describe('when using Common Cartridge version 1.3.0', () => { - const setup = () => { - const props = createCommonCartridgeWeblinkResourcePropsV130(); - const sut = new CommonCartridgeWebLinkResourceV130(props); - - return { sut }; - }; - - it('should return false', () => { - const { sut } = setup(); - - const result = sut.canInline(); - - expect(result).toBe(false); - }); - }); - }); - describe('getFilePath', () => { describe('when using Common Cartridge version 1.3.0', () => { const setup = () => { @@ -94,13 +75,38 @@ describe('CommonCartridgeWebLinkResourceV130', () => { notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; it('should throw error', () => { - expect(() => new CommonCartridgeWebLinkResourceV130(notSupportedProps)).toThrow(InternalServerErrorException); + expect(() => new CommonCartridgeWebLinkResourceV130(notSupportedProps)).toThrow( + VersionNotSupportedLoggableException + ); }); }); }); describe('getManifestXmlObject', () => { - describe('when using Common Cartridge version 1.3.0', () => { + describe('when creating organization xml object', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut, props }; + }; + + it('should return organization manifest fragment', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(CommonCartridgeElementType.ORGANIZATION); + + expect(result).toEqual({ + $: { + identifier: expect.any(String), + identifierref: props.identifier, + }, + title: props.title, + }); + }); + }); + + describe('when creating resource xml object', () => { const setup = () => { const props = createCommonCartridgeWeblinkResourcePropsV130(); const sut = new CommonCartridgeWebLinkResourceV130(props); @@ -108,10 +114,10 @@ describe('CommonCartridgeWebLinkResourceV130', () => { return { sut, props }; }; - it('should return the manifest XML object', () => { + it('should return resource manifest fragment', () => { const { sut, props } = setup(); - const result = sut.getManifestXmlObject(); + const result = sut.getManifestXmlObject(CommonCartridgeElementType.RESOURCE); expect(result).toEqual({ $: { @@ -127,4 +133,20 @@ describe('CommonCartridgeWebLinkResourceV130', () => { }); }); }); + + describe('when using unsupported element type', () => { + const setup = () => { + const unknownElementType = 'unknown' as CommonCartridgeElementType; + const props = createCommonCartridgeWeblinkResourcePropsV130(); + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut, unknownElementType }; + }; + + it('should throw error', () => { + const { sut, unknownElementType } = setup(); + + expect(() => sut.getManifestXmlObject(unknownElementType)).toThrow(ElementTypeNotSupportedLoggableException); + }); + }); }); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts index 1cfd8a3df5b..eb5d66ad8eb 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts @@ -1,6 +1,11 @@ -import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../../common-cartridge.enums'; -import { CommonCartridgeResource } from '../../interfaces'; -import { buildXmlString } from '../../utils'; +import { + CommonCartridgeElementType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { ElementTypeNotSupportedLoggableException } from '../../errors'; +import { CommonCartridgeResource, XmlObject } from '../../interfaces'; +import { buildXmlString, createIdentifier } from '../../utils'; export type CommonCartridgeWebLinkResourcePropsV130 = { type: CommonCartridgeResourceType.WEB_LINK; @@ -18,8 +23,19 @@ export class CommonCartridgeWebLinkResourceV130 extends CommonCartridgeResource super(props); } - public canInline(): boolean { - return false; + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(elementType: CommonCartridgeElementType): XmlObject { + switch (elementType) { + case CommonCartridgeElementType.RESOURCE: + return this.getManifestResourceXmlObject(); + case CommonCartridgeElementType.ORGANIZATION: + return this.getManifestOrganizationXmlObject(); + default: + throw new ElementTypeNotSupportedLoggableException(elementType); + } } public getFilePath(): string { @@ -47,11 +63,17 @@ export class CommonCartridgeWebLinkResourceV130 extends CommonCartridgeResource }); } - public getSupportedVersion(): CommonCartridgeVersion { - return CommonCartridgeVersion.V_1_3_0; + private getManifestOrganizationXmlObject(): XmlObject { + return { + $: { + identifier: createIdentifier(), + identifierref: this.props.identifier, + }, + title: this.props.title, + }; } - public getManifestXmlObject(): Record { + private getManifestResourceXmlObject(): XmlObject { return { $: { identifier: this.props.identifier, diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts index dca0baca323..90e0b5121ea 100644 --- a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts @@ -1,7 +1,7 @@ export { CommonCartridgeManifestResourcePropsV130 } from './common-cartridge-manifest-resource'; export { CommonCartridgeResourceFactoryV130 } from './common-cartridge-resource-factory'; export { - CommonCartridgeWebContentResourcePropsV130, CommonCartridgeWebContentResourceV130, + CommonCartridgeWebContentResourcePropsV130, } from './common-cartridge-web-content-resource'; export { CommonCartridgeWebLinkResourcePropsV130 } from './common-cartridge-web-link-resource'; diff --git a/apps/server/src/modules/common-cartridge/export/utils.spec.ts b/apps/server/src/modules/common-cartridge/export/utils.spec.ts index 3dae9ffa1c8..2a5b5b7de60 100644 --- a/apps/server/src/modules/common-cartridge/export/utils.spec.ts +++ b/apps/server/src/modules/common-cartridge/export/utils.spec.ts @@ -1,14 +1,5 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { ObjectId } from 'bson'; -import { CommonCartridgeVersion } from './common-cartridge.enums'; -import { - buildXmlString, - checkIntendedUse, - createElementTypeNotSupportedError, - createIdentifier, - createResourceTypeNotSupportedError, - createVersionNotSupportedError, -} from './utils'; +import { buildXmlString, createIdentifier } from './utils'; describe('CommonCartridgeUtils', () => { describe('buildXmlString', () => { @@ -19,17 +10,6 @@ describe('CommonCartridgeUtils', () => { }); }); - describe('createVersionNotSupportedError', () => { - describe('when creating error', () => { - it('should return error with message', () => { - const error = createVersionNotSupportedError(CommonCartridgeVersion.V_1_0_0); - - expect(error).toBeInstanceOf(InternalServerErrorException); - expect(error.message).toBe('Common Cartridge version 1.0.0 is not supported'); - }); - }); - }); - describe('createIdentifier', () => { describe('when creating identifier', () => { it('should return identifier with prefix', () => { @@ -43,42 +23,4 @@ describe('CommonCartridgeUtils', () => { }); }); }); - - describe('createResourceTypeNotSupportedError', () => { - describe('when creating error', () => { - it('should return error with message', () => { - const resourceType = 'unsupported'; - - const error = createResourceTypeNotSupportedError(resourceType); - - expect(error).toBeInstanceOf(InternalServerErrorException); - expect(error.message).toBe(`Common Cartridge resource type ${resourceType} is not supported`); - }); - }); - }); - - describe('createElementTypeNotSupportedError', () => { - describe('when creating error', () => { - it('should return error with message', () => { - const elementType = 'unsupported'; - - const error = createElementTypeNotSupportedError(elementType); - - expect(error).toBeInstanceOf(InternalServerErrorException); - expect(error.message).toBe(`Common Cartridge element type ${elementType} is not supported`); - }); - }); - }); - - describe('checkIntendedUse', () => { - describe('when intended use is not supported', () => { - it('should throw error', () => { - const supportedIntendedUses = ['use1', 'use2']; - - expect(() => checkIntendedUse('use3', supportedIntendedUses)).toThrowError( - 'Intended use use3 is not supported' - ); - }); - }); - }); }); diff --git a/apps/server/src/modules/common-cartridge/export/utils.ts b/apps/server/src/modules/common-cartridge/export/utils.ts index 0b110f783c5..0e22175216e 100644 --- a/apps/server/src/modules/common-cartridge/export/utils.ts +++ b/apps/server/src/modules/common-cartridge/export/utils.ts @@ -1,4 +1,3 @@ -import { InternalServerErrorException } from '@nestjs/common'; import { ObjectId } from 'bson'; import { Builder } from 'xml2js'; @@ -17,19 +16,6 @@ export function buildXmlString(obj: unknown): string { return xmlBuilder.buildObject(obj); } -export function createVersionNotSupportedError(version: string): Error { - return new InternalServerErrorException(`Common Cartridge version ${version} is not supported`); -} - -export function createResourceTypeNotSupportedError(type: string): Error { - return new InternalServerErrorException(`Common Cartridge resource type ${type} is not supported`); -} - -export function createElementTypeNotSupportedError(type: string): Error { - // AI next 1 line - return new InternalServerErrorException(`Common Cartridge element type ${type} is not supported`); -} - export function createIdentifier(identifier?: string | ObjectId): string { if (!identifier) { return `i${new ObjectId().toString()}`; @@ -37,9 +23,3 @@ export function createIdentifier(identifier?: string | ObjectId): string { return `i${identifier.toString()}`; } - -export function checkIntendedUse(intendedUse: string, supportedIntendedUses: string[]): void | never { - if (!supportedIntendedUses.includes(intendedUse)) { - throw new Error(`Intended use ${intendedUse} is not supported`); - } -} diff --git a/apps/server/src/modules/common-cartridge/index.ts b/apps/server/src/modules/common-cartridge/index.ts index 82b46a983b3..2f177a38fb6 100644 --- a/apps/server/src/modules/common-cartridge/index.ts +++ b/apps/server/src/modules/common-cartridge/index.ts @@ -1,11 +1,9 @@ export { CommonCartridgeFileBuilder, CommonCartridgeFileBuilderProps, + CommonCartridgeOrganizationProps, } from './export/builders/common-cartridge-file-builder'; -export { - CommonCartridgeOrganizationBuilder, - CommonCartridgeOrganizationBuilderOptions, -} from './export/builders/common-cartridge-organization-builder'; +export { CommonCartridgeOrganizationNode } from './export/builders/common-cartridge-organization-node'; export { CommonCartridgeElementType, CommonCartridgeIntendedUseType, @@ -19,8 +17,10 @@ export { CommonCartridgeFileParser } from './import/common-cartridge-file-parser export { CommonCartridgeResourceTypeV1P1 } from './import/common-cartridge-import.enums'; export { CommonCartridgeFileParserOptions, + CommonCartridgeOrganizationProps as CommonCartridgeImportOrganizationProps, CommonCartridgeResourceProps as CommonCartridgeImportResourceProps, - CommonCartridgeOrganizationProps, + CommonCartridgeWebContentResourceProps as CommonCartridgeImportWebContentResourceProps, + CommonCartridgeWebLinkResourceProps as CommonCartridgeImportWebLinkResourceProps, DEFAULT_FILE_PARSER_OPTIONS, } from './import/common-cartridge-import.types'; export { CommonCartridgeImportUtils } from './import/utils/common-cartridge-import-utils'; diff --git a/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts b/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts index 000ad586840..f6d05b9710b 100644 --- a/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts +++ b/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts @@ -1,5 +1,11 @@ import { faker } from '@faker-js/faker'; -import { CommonCartridgeElementType, CommonCartridgeVersion } from '@modules/common-cartridge'; +import { + CommonCartridgeElementProps, + CommonCartridgeElementType, + CommonCartridgeOrganizationProps, + CommonCartridgeVersion, +} from '@modules/common-cartridge'; +import { CommonCartridgeOrganizationNodeProps } from '../export/builders/common-cartridge-organization-node'; import { CommonCartridgeMetadataElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-metadata-element'; import { CommonCartridgeOrganizationElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-organization-element'; import { CommonCartridgeOrganizationsWrapperElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-organizations-wrapper-element'; @@ -94,3 +100,28 @@ export function createCommonCartridgeResourcesWrapperElementPropsV130( items: items || [], }; } + +export function createCommonCartridgeMetadataElementProps(): CommonCartridgeElementProps { + return { + type: CommonCartridgeElementType.METADATA, + title: faker.lorem.words(), + creationDate: new Date(), + copyrightOwners: ['John Doe', 'Jane Doe'], + }; +} + +export function createCommonCartridgeOrganizationProps(): CommonCartridgeOrganizationProps { + return { + title: faker.lorem.words(), + identifier: faker.string.uuid(), + }; +} + +export function createCommonCartridgeOrganizationNodeProps(): CommonCartridgeOrganizationNodeProps { + return { + type: CommonCartridgeElementType.ORGANIZATION, + identifier: faker.string.uuid(), + title: faker.lorem.words(), + version: CommonCartridgeVersion.V_1_1_0, + }; +} diff --git a/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts b/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts index cbe4f31f0c6..136f0e8ac6f 100644 --- a/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts +++ b/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts @@ -1,16 +1,21 @@ import { faker } from '@faker-js/faker'; import { CommonCartridgeIntendedUseType, + CommonCartridgeResourceProps, CommonCartridgeResourceType, CommonCartridgeVersion, } from '@modules/common-cartridge'; -import { CommonCartridgeElement } from '../export/interfaces/common-cartridge-element.interface'; +import { CommonCartridgeElementFactory } from '../export/elements/common-cartridge-element-factory'; import { CommonCartridgeManifestResourcePropsV110 } from '../export/resources/v1.1.0/common-cartridge-manifest-resource'; import { CommonCartridgeWebContentResourcePropsV110 } from '../export/resources/v1.1.0/common-cartridge-web-content-resource'; import { CommonCartridgeWebLinkResourcePropsV110 } from '../export/resources/v1.1.0/common-cartridge-web-link-resource'; import { CommonCartridgeManifestResourcePropsV130 } from '../export/resources/v1.3.0/common-cartridge-manifest-resource'; import { CommonCartridgeWebContentResourcePropsV130 } from '../export/resources/v1.3.0/common-cartridge-web-content-resource'; import { CommonCartridgeWebLinkResourcePropsV130 } from '../export/resources/v1.3.0/common-cartridge-web-link-resource'; +import { + createCommonCartridgeMetadataElementPropsV110, + createCommonCartridgeMetadataElementPropsV130, +} from './common-cartridge-element-props.factory'; export function createCommonCartridgeWeblinkResourcePropsV110(): CommonCartridgeWebLinkResourcePropsV110 { return { @@ -63,7 +68,7 @@ export function createCommonCartridgeManifestResourcePropsV110(): CommonCartridg type: CommonCartridgeResourceType.MANIFEST, version: CommonCartridgeVersion.V_1_1_0, identifier: faker.string.uuid(), - metadata: {} as CommonCartridgeElement, + metadata: CommonCartridgeElementFactory.createElement(createCommonCartridgeMetadataElementPropsV110()), organizations: [], resources: [], }; @@ -74,8 +79,27 @@ export function createCommonCartridgeManifestResourcePropsV130(): CommonCartridg type: CommonCartridgeResourceType.MANIFEST, version: CommonCartridgeVersion.V_1_3_0, identifier: faker.string.uuid(), - metadata: {} as CommonCartridgeElement, + metadata: CommonCartridgeElementFactory.createElement(createCommonCartridgeMetadataElementPropsV130()), organizations: [], resources: [], }; } + +export function createCommonCartridgeWebLinkResourceProps(): CommonCartridgeResourceProps { + return { + type: CommonCartridgeResourceType.WEB_LINK, + title: faker.lorem.words(), + identifier: faker.string.uuid(), + url: faker.internet.url(), + }; +} + +export function createCommonCartridgeWebContentResourceProps(): CommonCartridgeResourceProps { + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + title: faker.lorem.words(), + identifier: faker.string.uuid(), + html: faker.lorem.paragraph(), + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; +} diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 3627b94b129..5f112d07dff 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -18,8 +18,8 @@ import { import { LoggerModule } from '@src/core/logger'; import { BoardNodeRepo } from '../board/repo'; import { COURSE_REPO } from './domain'; +import { CommonCartridgeExportMapper } from './mapper/common-cartridge-export.mapper'; import { CommonCartridgeImportMapper } from './mapper/common-cartridge-import.mapper'; -import { CommonCartridgeMapper } from './mapper/common-cartridge.mapper'; import { ColumnBoardNodeRepo } from './repo'; import { CourseMikroOrmRepo } from './repo/mikro-orm/course.repo'; import { @@ -57,7 +57,7 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; CommonCartridgeExportService, CommonCartridgeFileValidatorPipe, CommonCartridgeImportService, - CommonCartridgeMapper, + CommonCartridgeExportMapper, CommonCartridgeImportMapper, CourseCopyService, CourseGroupRepo, diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge-export.mapper.spec.ts similarity index 96% rename from apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts rename to apps/server/src/modules/learnroom/mapper/common-cartridge-export.mapper.spec.ts index 2fe6f0967a1..0e4bdc92e82 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge-export.mapper.spec.ts @@ -5,7 +5,7 @@ import { CommonCartridgeElementType, CommonCartridgeFileBuilderProps, CommonCartridgeIntendedUseType, - CommonCartridgeOrganizationBuilderOptions, + CommonCartridgeOrganizationProps, CommonCartridgeResourceProps, CommonCartridgeResourceType, CommonCartridgeVersion, @@ -18,25 +18,25 @@ import { ComponentProperties, ComponentType } from '@shared/domain/entity'; import { courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; import { linkElementFactory, richTextElementFactory } from '@modules/board/testing'; import { LearnroomConfig } from '../learnroom.config'; -import { CommonCartridgeMapper } from './common-cartridge.mapper'; +import { CommonCartridgeExportMapper } from './common-cartridge-export.mapper'; -describe('CommonCartridgeMapper', () => { +describe('CommonCartridgeExportMapper', () => { let module: TestingModule; - let sut: CommonCartridgeMapper; + let sut: CommonCartridgeExportMapper; let configServiceMock: DeepMocked>; beforeAll(async () => { await setupEntities(); module = await Test.createTestingModule({ providers: [ - CommonCartridgeMapper, + CommonCartridgeExportMapper, { provide: ConfigService, useValue: createMock>(), }, ], }).compile(); - sut = module.get(CommonCartridgeMapper); + sut = module.get(CommonCartridgeExportMapper); configServiceMock = module.get(ConfigService); }); @@ -88,7 +88,7 @@ describe('CommonCartridgeMapper', () => { const { lesson } = setup(); const organizationProps = sut.mapLessonToOrganization(lesson); - expect(organizationProps).toStrictEqual>({ + expect(organizationProps).toStrictEqual>({ identifier: createIdentifier(lesson.id), title: lesson.name, }); @@ -117,7 +117,7 @@ describe('CommonCartridgeMapper', () => { const { componentProps } = setup(); const organizationProps = sut.mapContentToOrganization(componentProps); - expect(organizationProps).toStrictEqual>({ + expect(organizationProps).toStrictEqual>({ identifier: expect.any(String), title: componentProps.title, }); @@ -188,7 +188,7 @@ describe('CommonCartridgeMapper', () => { const { task } = setup(); const organizationProps = sut.mapTaskToOrganization(task); - expect(organizationProps).toStrictEqual>({ + expect(organizationProps).toStrictEqual>({ identifier: expect.any(String), title: task.name, }); @@ -415,10 +415,7 @@ describe('CommonCartridgeMapper', () => { expect(resourceProps).toStrictEqual({ type: CommonCartridgeResourceType.WEB_CONTENT, identifier: expect.any(String), - title: richTextElement.text - .slice(0, 50) - .replace(/<[^>]*>?/gm, '') - .concat('...'), + title: richTextElement.text.slice(0, 50).replace(/<[^>]*>?/gm, ''), html: `

${richTextElement.text}

`, intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, }); diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge-export.mapper.ts similarity index 88% rename from apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts rename to apps/server/src/modules/learnroom/mapper/common-cartridge-export.mapper.ts index 3d684905903..989d230c3eb 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge-export.mapper.ts @@ -2,21 +2,21 @@ import { LinkElement, RichTextElement } from '@modules/board/domain'; import { CommonCartridgeElementProps, CommonCartridgeElementType, - CommonCartridgeFileBuilderProps, CommonCartridgeIntendedUseType, - CommonCartridgeOrganizationBuilderOptions, CommonCartridgeResourceProps, CommonCartridgeResourceType, CommonCartridgeVersion, createIdentifier, } from '@modules/common-cartridge'; +import { CommonCartridgeOrganizationProps } from '@modules/common-cartridge/export/builders/common-cartridge-file-builder'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ComponentProperties, ComponentType, Course, LessonEntity, Task } from '@shared/domain/entity'; +import sanitizeHtml from 'sanitize-html'; import { LearnroomConfig } from '../learnroom.config'; @Injectable() -export class CommonCartridgeMapper { +export class CommonCartridgeExportMapper { constructor(private readonly configService: ConfigService) {} public mapCourseToMetadata(course: Course): CommonCartridgeElementProps { @@ -28,21 +28,21 @@ export class CommonCartridgeMapper { }; } - public mapLessonToOrganization(lesson: LessonEntity): CommonCartridgeOrganizationBuilderOptions { + public mapLessonToOrganization(lesson: LessonEntity): CommonCartridgeOrganizationProps { return { identifier: createIdentifier(lesson.id), title: lesson.name, }; } - public mapContentToOrganization(content: ComponentProperties): CommonCartridgeOrganizationBuilderOptions { + public mapContentToOrganization(content: ComponentProperties): CommonCartridgeOrganizationProps { return { identifier: createIdentifier(content._id), title: content.title, }; } - public mapTaskToOrganization(task: Task): CommonCartridgeOrganizationBuilderOptions { + public mapTaskToOrganization(task: Task): CommonCartridgeOrganizationProps { return { identifier: createIdentifier(), title: task.name, @@ -112,7 +112,10 @@ export class CommonCartridgeMapper { } } - public mapCourseToManifest(version: CommonCartridgeVersion, course: Course): CommonCartridgeFileBuilderProps { + public mapCourseToManifest( + version: CommonCartridgeVersion, + course: Course + ): { version: CommonCartridgeVersion; identifier: string } { return { version, identifier: createIdentifier(course.id), @@ -139,10 +142,11 @@ export class CommonCartridgeMapper { } private getTextTitle(text: string): string { - const title = text - .slice(0, 50) - .replace(/<[^>]*>?/gm, '') - .concat('...'); + const title = sanitizeHtml(text, { + allowedTags: [], + allowedAttributes: {}, + }).slice(0, 50); + return title; } } diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts index c9a4f0d544f..a2515c4d367 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts @@ -1,12 +1,11 @@ import { faker } from '@faker-js/faker'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ContentElementType } from '@modules/board/domain'; -import { LinkContentBody, RichTextContentBody } from '@modules/board/controller/dto'; +import { ContentElementType, LinkContentBody, RichTextContentBody } from '@modules/board'; import { + CommonCartridgeImportOrganizationProps, CommonCartridgeImportResourceProps, - CommonCartridgeOrganizationProps, CommonCartridgeResourceTypeV1P1, } from '@modules/common-cartridge'; +import { Test, TestingModule } from '@nestjs/testing'; import { InputFormat } from '@shared/domain/types'; import { CommonCartridgeImportMapper } from './common-cartridge-import.mapper'; @@ -15,7 +14,7 @@ describe('CommonCartridgeImportMapper', () => { let sut: CommonCartridgeImportMapper; const setupOrganization = () => { - const organization: CommonCartridgeOrganizationProps = { + const organization: CommonCartridgeImportOrganizationProps = { path: faker.string.uuid(), pathDepth: faker.number.int({ min: 0, max: 3 }), identifier: faker.string.uuid(), diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts index e199bf466a9..826186cd4e8 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts @@ -1,13 +1,13 @@ +import { AnyElementContentBody, ContentElementType, LinkContentBody, RichTextContentBody } from '@modules/board'; +import { + CommonCartridgeImportResourceProps, + CommonCartridgeImportWebContentResourceProps, + CommonCartridgeImportWebLinkResourceProps, + CommonCartridgeOrganizationProps, + CommonCartridgeResourceTypeV1P1, +} from '@modules/common-cartridge'; import { Injectable } from '@nestjs/common'; -import { ContentElementType } from '@modules/board/domain'; import { InputFormat } from '@shared/domain/types'; -import { AnyElementContentBody, LinkContentBody, RichTextContentBody } from '@modules/board/controller/dto'; -import { CommonCartridgeOrganizationProps, CommonCartridgeResourceTypeV1P1 } from '@modules/common-cartridge'; -import { - CommonCartridgeResourceProps, - CommonCartridgeWebContentResourceProps, - CommonCartridgeWebLinkResourceProps, -} from '@src/modules/common-cartridge/import/common-cartridge-import.types'; @Injectable() export class CommonCartridgeImportMapper { @@ -45,7 +45,7 @@ export class CommonCartridgeImportMapper { } } - public mapResourceToContentElementBody(resource: CommonCartridgeResourceProps): AnyElementContentBody { + public mapResourceToContentElementBody(resource: CommonCartridgeImportResourceProps): AnyElementContentBody { switch (resource.type) { case CommonCartridgeResourceTypeV1P1.WEB_LINK: return this.createLinkContentElementBody(resource); @@ -56,7 +56,7 @@ export class CommonCartridgeImportMapper { } } - private createLinkContentElementBody(resource: CommonCartridgeWebLinkResourceProps): AnyElementContentBody { + private createLinkContentElementBody(resource: CommonCartridgeImportWebLinkResourceProps): AnyElementContentBody { const body = new LinkContentBody(); body.title = resource.title; @@ -65,7 +65,7 @@ export class CommonCartridgeImportMapper { return body; } - private createWebContentElementBody(resource: CommonCartridgeWebContentResourceProps): AnyElementContentBody { + private createWebContentElementBody(resource: CommonCartridgeImportWebContentResourceProps): AnyElementContentBody { const body = new RichTextContentBody(); body.text = resource.html; diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts index 605d9ad3cdc..4cd4ff7fab0 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts @@ -1,5 +1,13 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ColumnBoardService } from '@modules/board'; +import { + cardFactory, + columnBoardFactory, + columnFactory, + linkElementFactory, + richTextElementFactory, +} from '@modules/board/testing'; import { CommonCartridgeVersion } from '@modules/common-cartridge'; import { CommonCartridgeExportService, CourseService, LearnroomConfig } from '@modules/learnroom'; import { LessonService } from '@modules/lesson'; @@ -8,16 +16,8 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ComponentType } from '@shared/domain/entity'; import { courseFactory, lessonFactory, setupEntities, taskFactory } from '@shared/testing'; -import { ColumnBoardService } from '@src/modules/board'; -import { - cardFactory, - columnBoardFactory, - columnFactory, - linkElementFactory, - richTextElementFactory, -} from '@src/modules/board/testing'; import AdmZip from 'adm-zip'; -import { CommonCartridgeMapper } from '../mapper/common-cartridge.mapper'; +import { CommonCartridgeExportMapper } from '../mapper/common-cartridge-export.mapper'; describe('CommonCartridgeExportService', () => { let module: TestingModule; @@ -107,7 +107,7 @@ describe('CommonCartridgeExportService', () => { module = await Test.createTestingModule({ providers: [ CommonCartridgeExportService, - CommonCartridgeMapper, + CommonCartridgeExportMapper, { provide: CourseService, useValue: createMock(), diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts index 4c146bcfeb6..3300d60af1c 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts @@ -3,24 +3,24 @@ import { BoardExternalReferenceType, Card, Column, + ColumnBoardService, isCard, isColumn, isLinkElement, isRichTextElement, -} from '@modules/board/domain'; +} from '@modules/board'; import { CommonCartridgeFileBuilder, - CommonCartridgeOrganizationBuilder, + CommonCartridgeOrganizationNode, CommonCartridgeVersion, + createIdentifier, } from '@modules/common-cartridge'; import { LessonService } from '@modules/lesson'; import { TaskService } from '@modules/task'; import { Injectable } from '@nestjs/common'; import { ComponentProperties } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; -import { ColumnBoardService } from '@src/modules/board'; -import { createIdentifier } from '@src/modules/common-cartridge/export/utils'; -import { CommonCartridgeMapper } from '../mapper/common-cartridge.mapper'; +import { CommonCartridgeExportMapper } from '../mapper/common-cartridge-export.mapper'; import { CourseService } from './course.service'; @Injectable() @@ -30,7 +30,7 @@ export class CommonCartridgeExportService { private readonly lessonService: LessonService, private readonly taskService: TaskService, private readonly columnBoardService: ColumnBoardService, - private readonly commonCartridgeMapper: CommonCartridgeMapper + private readonly mapper: CommonCartridgeExportMapper ) {} public async exportCourse( @@ -42,9 +42,9 @@ export class CommonCartridgeExportService { exportedColumnBoards: string[] ): Promise { const course = await this.courseService.findById(courseId); - const builder = new CommonCartridgeFileBuilder(this.commonCartridgeMapper.mapCourseToManifest(version, course)); + const builder = new CommonCartridgeFileBuilder(this.mapper.mapCourseToManifest(version, course)); - builder.addMetadata(this.commonCartridgeMapper.mapCourseToMetadata(course)); + builder.addMetadata(this.mapper.mapCourseToMetadata(course)); await this.addLessons(builder, courseId, version, exportedTopics); await this.addTasks(builder, courseId, userId, version, exportedTasks); @@ -66,14 +66,14 @@ export class CommonCartridgeExportService { return; } - const organizationBuilder = builder.addOrganization(this.commonCartridgeMapper.mapLessonToOrganization(lesson)); + const lessonOrganization = builder.createOrganization(this.mapper.mapLessonToOrganization(lesson)); lesson.contents.forEach((content) => { - this.addComponentToOrganization(organizationBuilder, content); + this.addComponentToOrganization(content, lessonOrganization); }); lesson.getLessonLinkedTasks().forEach((task) => { - organizationBuilder.addResource(this.commonCartridgeMapper.mapTaskToResource(task, version)); + lessonOrganization.addResource(this.mapper.mapTaskToResource(task, version)); }); }); } @@ -91,8 +91,8 @@ export class CommonCartridgeExportService { return; } - const organization = builder.addOrganization({ - title: '', + const tasksOrganization = builder.createOrganization({ + title: 'Aufgaben', identifier: createIdentifier(), }); @@ -101,7 +101,7 @@ export class CommonCartridgeExportService { return; } - organization.addResource(this.commonCartridgeMapper.mapTaskToResource(task, version)); + tasksOrganization.addResource(this.mapper.mapTaskToResource(task, version)); }); } @@ -118,21 +118,21 @@ export class CommonCartridgeExportService { ).filter((cb) => exportedColumnBoards.includes(cb.id)); for (const columnBoard of columnBoards) { - const organization = builder.addOrganization({ + const columnBoardOrganization = builder.createOrganization({ title: columnBoard.title, identifier: createIdentifier(columnBoard.id), }); columnBoard.children .filter((child) => isColumn(child)) - .forEach((column) => this.addColumnToOrganization(column as Column, organization)); + .forEach((column) => this.addColumnToOrganization(column as Column, columnBoardOrganization)); } } - private addColumnToOrganization(column: Column, organizationBuilder: CommonCartridgeOrganizationBuilder): void { + private addColumnToOrganization(column: Column, columnBoardOrganization: CommonCartridgeOrganizationNode): void { const { id } = column; - const columnOrganization = organizationBuilder.addSubOrganization({ - title: column.title ?? '', + const columnOrganization = columnBoardOrganization.createChild({ + title: column.title || '', identifier: createIdentifier(id), }); @@ -141,49 +141,43 @@ export class CommonCartridgeExportService { .forEach((card) => this.addCardToOrganization(card as Card, columnOrganization)); } - private addCardToOrganization(card: Card, organizationBuilder: CommonCartridgeOrganizationBuilder): void { - const { id } = card; - const cardOrganization = organizationBuilder.addSubOrganization({ - title: card.title ?? '', - identifier: createIdentifier(id), + private addCardToOrganization(card: Card, columnOrganization: CommonCartridgeOrganizationNode): void { + const cardOrganization = columnOrganization.createChild({ + title: card.title || '', + identifier: createIdentifier(card.id), }); card.children.forEach((child) => this.addCardElementToOrganization(child, cardOrganization)); } - private addCardElementToOrganization( - element: AnyBoardNode, - organizationBuilder: CommonCartridgeOrganizationBuilder - ): void { + private addCardElementToOrganization(element: AnyBoardNode, cardOrganization: CommonCartridgeOrganizationNode): void { if (isRichTextElement(element)) { - const resource = this.commonCartridgeMapper.mapRichTextElementToResource(element); + const resource = this.mapper.mapRichTextElementToResource(element); - organizationBuilder.addResource(resource); + cardOrganization.addResource(resource); } if (isLinkElement(element)) { - const resource = this.commonCartridgeMapper.mapLinkElementToResource(element); + const resource = this.mapper.mapLinkElementToResource(element); - organizationBuilder.addResource(resource); + cardOrganization.addResource(resource); } } private addComponentToOrganization( - organizationBuilder: CommonCartridgeOrganizationBuilder, - component: ComponentProperties + component: ComponentProperties, + lessonOrganization: CommonCartridgeOrganizationNode ): void { - const resources = this.commonCartridgeMapper.mapContentToResources(component); + const resources = this.mapper.mapContentToResources(component); if (Array.isArray(resources)) { - const subOrganizationBuilder = organizationBuilder.addSubOrganization( - this.commonCartridgeMapper.mapContentToOrganization(component) - ); + const componentOrganization = lessonOrganization.createChild(this.mapper.mapContentToOrganization(component)); resources.forEach((resource) => { - subOrganizationBuilder.addResource(resource); + componentOrganization.addResource(resource); }); } else { - organizationBuilder.addResource(resources); + lessonOrganization.addResource(resources); } } } diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts index 1cded6df376..af55a6eb2a9 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts @@ -2,19 +2,19 @@ import { BoardExternalReferenceType, BoardLayout, BoardNodeFactory, + BoardNodeService, Card, - ContentElementType, Column, ColumnBoard, -} from '@modules/board/domain'; -import { BoardNodeService } from '@modules/board/service'; -import { Injectable } from '@nestjs/common'; -import { Course, User } from '@shared/domain/entity'; + ContentElementType, +} from '@modules/board'; import { CommonCartridgeFileParser, - CommonCartridgeOrganizationProps, + CommonCartridgeImportOrganizationProps, DEFAULT_FILE_PARSER_OPTIONS, -} from '@src/modules/common-cartridge'; +} from '@modules/common-cartridge'; +import { Injectable } from '@nestjs/common'; +import { Course, User } from '@shared/domain/entity'; import { CommonCartridgeImportMapper } from '../mapper/common-cartridge-import.mapper'; import { CourseService } from './course.service'; @@ -47,8 +47,8 @@ export class CommonCartridgeImportService { private async createColumnBoard( parser: CommonCartridgeFileParser, course: Course, - boardProps: CommonCartridgeOrganizationProps, - organizations: CommonCartridgeOrganizationProps[] + boardProps: CommonCartridgeImportOrganizationProps, + organizations: CommonCartridgeImportOrganizationProps[] ): Promise { const columnBoard = this.boardNodeFactory.buildColumnBoard({ context: { @@ -66,8 +66,8 @@ export class CommonCartridgeImportService { private async createColumns( parser: CommonCartridgeFileParser, columnBoard: ColumnBoard, - boardProps: CommonCartridgeOrganizationProps, - organizations: CommonCartridgeOrganizationProps[] + boardProps: CommonCartridgeImportOrganizationProps, + organizations: CommonCartridgeImportOrganizationProps[] ): Promise { const columnsWithResource = organizations.filter( (organization) => @@ -91,7 +91,7 @@ export class CommonCartridgeImportService { private async createColumnWithResource( parser: CommonCartridgeFileParser, columnBoard: ColumnBoard, - columnProps: CommonCartridgeOrganizationProps + columnProps: CommonCartridgeImportOrganizationProps ): Promise { const column = this.boardNodeFactory.buildColumn(); const { title } = this.mapper.mapOrganizationToColumn(columnProps); @@ -103,8 +103,8 @@ export class CommonCartridgeImportService { private async createColumn( parser: CommonCartridgeFileParser, columnBoard: ColumnBoard, - columnProps: CommonCartridgeOrganizationProps, - organizations: CommonCartridgeOrganizationProps[] + columnProps: CommonCartridgeImportOrganizationProps, + organizations: CommonCartridgeImportOrganizationProps[] ): Promise { const column = this.boardNodeFactory.buildColumn(); const { title } = this.mapper.mapOrganizationToColumn(columnProps); @@ -131,7 +131,7 @@ export class CommonCartridgeImportService { private async createCardWithElement( parser: CommonCartridgeFileParser, column: Column, - cardProps: CommonCartridgeOrganizationProps, + cardProps: CommonCartridgeImportOrganizationProps, withTitle = true ): Promise { const card = this.boardNodeFactory.buildCard(); @@ -154,8 +154,8 @@ export class CommonCartridgeImportService { private async createCard( parser: CommonCartridgeFileParser, column: Column, - cardProps: CommonCartridgeOrganizationProps, - organizations: CommonCartridgeOrganizationProps[] + cardProps: CommonCartridgeImportOrganizationProps, + organizations: CommonCartridgeImportOrganizationProps[] ) { const card = this.boardNodeFactory.buildCard(); const { title, height } = this.mapper.mapOrganizationToCard(cardProps, true); @@ -175,7 +175,7 @@ export class CommonCartridgeImportService { private async createCardElement( parser: CommonCartridgeFileParser, card: Card, - cardElementProps: CommonCartridgeOrganizationProps + cardElementProps: CommonCartridgeImportOrganizationProps ) { if (cardElementProps.isResource) { const resource = parser.getResource(cardElementProps); diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts index cbd8bee44d6..91f85457879 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts @@ -4,7 +4,7 @@ import { LessonCopyService } from '@modules/lesson/service'; import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { ToolFeatures } from '@modules/tool/tool-config'; +import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; import { Test, TestingModule } from '@nestjs/testing'; import { Course } from '@shared/domain/entity'; import { CourseRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; @@ -16,7 +16,6 @@ import { setupEntities, userFactory, } from '@shared/testing'; -import { IToolFeatures } from '@src/modules/tool/tool-config'; import { contextExternalToolFactory } from '../../tool/context-external-tool/testing'; import { BoardCopyService } from './board-copy.service'; import { CourseCopyService } from './course-copy.service'; diff --git a/apps/server/src/modules/learnroom/uc/course-import.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-import.uc.spec.ts index eedae71a09f..fe50b3f5926 100644 --- a/apps/server/src/modules/learnroom/uc/course-import.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-import.uc.spec.ts @@ -1,12 +1,12 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MikroORM } from '@mikro-orm/core'; +import { AuthorizationService } from '@modules/authorization'; import { NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { courseFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationService } from '@src/modules/authorization'; import { LearnroomConfig } from '../learnroom.config'; import { CommonCartridgeImportService } from '../service'; import { CourseImportUc } from './course-import.uc'; diff --git a/apps/server/src/modules/learnroom/uc/course-import.uc.ts b/apps/server/src/modules/learnroom/uc/course-import.uc.ts index 4eccabe8132..33b0e9a3a63 100644 --- a/apps/server/src/modules/learnroom/uc/course-import.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-import.uc.ts @@ -1,8 +1,8 @@ +import { AuthorizationService } from '@modules/authorization'; import { Injectable, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { AuthorizationService } from '@src/modules/authorization'; import { LearnroomConfig } from '../learnroom.config'; import { CommonCartridgeImportService } from '../service'; diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index 29b0a5be59b..3c2a082fd4a 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -1,12 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationService } from '@modules/authorization'; +import { RoleDto, RoleService } from '@modules/role'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; import { courseFactory, setupEntities, UserAndAccountTestFactory } from '@shared/testing'; -import { AuthorizationService } from '@src/modules/authorization'; -import { RoleDto, RoleService } from '@src/modules/role'; -import { CourseUc } from './course.uc'; import { CourseService } from '../service'; +import { CourseUc } from './course.uc'; describe('CourseUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index a60676803d9..ed7abf98a1d 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -1,11 +1,11 @@ +import { AuthorizationService } from '@modules/authorization'; +import { RoleService } from '@modules/role'; import { Injectable } from '@nestjs/common'; import { PaginationParams } from '@shared/controller/'; import { Course } from '@shared/domain/entity'; import { SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; -import { AuthorizationService } from '@src/modules/authorization'; -import { RoleService } from '@src/modules/role'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseService } from '../service'; From 6bb7eaee358d92fe622495f69ae59427646f1d2b Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Wed, 26 Jun 2024 13:54:50 +0200 Subject: [PATCH 02/35] N21-2022 close wizard with migration (#5072) --- apps/server/src/modules/user-import/index.ts | 1 + .../service/user-import.service.spec.ts | 54 +++++++- .../service/user-import.service.ts | 19 ++- .../user-import/uc/user-import.uc.spec.ts | 36 +----- .../modules/user-import/uc/user-import.uc.ts | 12 +- .../modules/user-import/user-import.module.ts | 2 +- .../api-test/user-login-migration.api.spec.ts | 63 +++++++++- .../user-login-migration.controller.ts | 9 +- .../uc/close-migration-wizard.uc.spec.ts | 118 ++++++++++++++++++ .../uc/close-migration-wizard.uc.ts | 28 +++++ .../uc/close-user-login-migration.uc.ts | 2 +- .../modules/user-login-migration/uc/index.ts | 1 + .../user-login-migration-api.module.ts | 4 + 13 files changed, 298 insertions(+), 51 deletions(-) create mode 100644 apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.spec.ts create mode 100644 apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.ts diff --git a/apps/server/src/modules/user-import/index.ts b/apps/server/src/modules/user-import/index.ts index b6cd58577bf..0d666df6a20 100644 --- a/apps/server/src/modules/user-import/index.ts +++ b/apps/server/src/modules/user-import/index.ts @@ -1,3 +1,4 @@ export { ImportUserModule } from './user-import.module'; export { UserImportConfigModule } from './user-import-config.module'; export { IUserImportFeatures, UserImportConfiguration } from './config'; +export { UserImportService } from './service'; diff --git a/apps/server/src/modules/user-import/service/user-import.service.spec.ts b/apps/server/src/modules/user-import/service/user-import.service.spec.ts index 397d042eaea..adfbba48f6d 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.spec.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -19,7 +20,7 @@ import { } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { UserMigrationIsNotEnabled } from '../loggable'; +import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; import { UserImportService } from './user-import.service'; describe(UserImportService.name, () => { @@ -31,6 +32,7 @@ describe(UserImportService.name, () => { let legacySystemRepo: DeepMocked; let userService: DeepMocked; let logger: DeepMocked; + let schoolService: DeepMocked; const features: IUserImportFeatures = { userMigrationSystemId: new ObjectId().toHexString(), @@ -65,6 +67,10 @@ describe(UserImportService.name, () => { provide: Logger, useValue: createMock(), }, + { + provide: LegacySchoolService, + useValue: createMock(), + }, ], }).compile(); @@ -74,6 +80,7 @@ describe(UserImportService.name, () => { legacySystemRepo = module.get(LegacySystemRepo); userService = module.get(UserService); logger = module.get(Logger); + schoolService = module.get(LegacySchoolService); }); afterAll(async () => { @@ -356,4 +363,49 @@ describe(UserImportService.name, () => { }); }); }); + + describe('resetMigrationForUsersSchool', () => { + describe('when resetting the migration for a school', () => { + const setup = () => { + const currentUser: User = userFactory.build(); + const school: LegacySchoolDo = legacySchoolDoFactory.build(); + + return { + currentUser, + school, + }; + }; + + it('should delete import users for school', async () => { + const { currentUser, school } = setup(); + + await service.resetMigrationForUsersSchool(currentUser, school); + + expect(importUserRepo.deleteImportUsersBySchool).toHaveBeenCalledWith(currentUser.school); + }); + + it('should save school with reset migration flags', async () => { + const { currentUser, school } = setup(); + + await service.resetMigrationForUsersSchool(currentUser, school); + + expect(schoolService.save).toHaveBeenCalledWith( + { + ...school, + inUserMigration: undefined, + inMaintenanceSince: undefined, + }, + true + ); + }); + + it('should log canceled migration', async () => { + const { currentUser, school } = setup(); + + await service.resetMigrationForUsersSchool(currentUser, school); + + expect(logger.notice).toHaveBeenCalledWith(new UserMigrationCanceledLoggable(expect.any(LegacySchoolDo))); + }); + }); + }); }); diff --git a/apps/server/src/modules/user-import/service/user-import.service.ts b/apps/server/src/modules/user-import/service/user-import.service.ts index 0e165d4828c..2f6d8ef6c49 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.ts @@ -1,12 +1,13 @@ +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { SchoolFeature } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo } from '@shared/repo'; -import { UserService } from '@modules/user'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { UserMigrationIsNotEnabled } from '../loggable'; +import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; @Injectable() export class UserImportService { @@ -15,7 +16,8 @@ export class UserImportService { private readonly systemRepo: LegacySystemRepo, private readonly userService: UserService, @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, - private readonly logger: Logger + private readonly logger: Logger, + private readonly schoolService: LegacySchoolService ) {} public async saveImportUsers(importUsers: ImportUser[]): Promise { @@ -74,4 +76,15 @@ export class UserImportService { public async deleteImportUsersBySchool(school: SchoolEntity): Promise { await this.userImportRepo.deleteImportUsersBySchool(school); } + + public async resetMigrationForUsersSchool(currentUser: User, school: LegacySchoolDo): Promise { + await this.userImportRepo.deleteImportUsersBySchool(currentUser.school); + + school.inUserMigration = undefined; + school.inMaintenanceSince = undefined; + + await this.schoolService.save(school, true); + + this.logger.notice(new UserMigrationCanceledLoggable(school)); + } } 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 81ed9b6502f..705531a3e90 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 @@ -3,6 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Account, AccountService } from '@modules/account'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; @@ -26,14 +27,8 @@ import { } from '@shared/testing'; import { systemEntityFactory } from '@shared/testing/factory/systemEntityFactory'; import { Logger } from '@src/core/logger'; -import { UserService } from '@modules/user'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { - SchoolNotMigratedLoggableException, - UserAlreadyMigratedLoggable, - UserMigrationCanceledLoggable, -} from '../loggable'; - +import { SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable } from '../loggable'; import { UserImportService } from '../service'; import { LdapAlreadyPersistedException, @@ -1195,35 +1190,12 @@ describe('[ImportUserModule]', () => { expect(userImportService.checkFeatureEnabled).toHaveBeenCalled(); }); - it('should delete import users for school', async () => { - const { user } = setup(); - - await uc.cancelMigration(user.id); - - expect(importUserRepo.deleteImportUsersBySchool).toHaveBeenCalledWith(user.school); - }); - - it('should save school with reset migration flags', async () => { + it('should call reset migration', async () => { const { user, school } = setup(); await uc.cancelMigration(user.id); - expect(schoolService.save).toHaveBeenCalledWith( - { - ...school, - inUserMigration: undefined, - inMaintenanceSince: undefined, - }, - true - ); - }); - - it('should log canceled migration', async () => { - const { user } = setup(); - - await uc.cancelMigration(user.id); - - expect(logger.notice).toHaveBeenCalledWith(new UserMigrationCanceledLoggable(expect.any(LegacySchoolDo))); + expect(userImportService.resetMigrationForUsersSchool).toHaveBeenCalledWith(user, school); }); }); }); diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index 06ef3403eb0..a77127c51e4 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -1,6 +1,7 @@ import { Account, AccountSave, AccountService } from '@modules/account'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; @@ -12,7 +13,6 @@ import { IFindOptions, Permission } from '@shared/domain/interface'; import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { UserService } from '@modules/user'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { MigrationMayBeCompleted, @@ -22,7 +22,6 @@ import { SchoolInUserMigrationStartLoggable, SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable, - UserMigrationCanceledLoggable, } from '../loggable'; import { UserImportService } from '../service'; @@ -332,14 +331,7 @@ export class UserImportUc { this.userImportService.checkFeatureEnabled(school); - await this.importUserRepo.deleteImportUsersBySchool(currentUser.school); - - school.inUserMigration = undefined; - school.inMaintenanceSince = undefined; - - await this.schoolService.save(school, true); - - this.logger.notice(new UserMigrationCanceledLoggable(school)); + await this.userImportService.resetMigrationForUsersSchool(currentUser, school); } private async getCurrentUser(currentUserId: EntityId, permission: UserImportPermissions): Promise { diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index 7caed266954..6c305a98353 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -38,7 +38,7 @@ import { UserImportConfigModule } from './user-import-config.module'; UserImportService, SchulconnexFetchImportUsersService, ], - exports: [], + exports: [UserImportService], }) /** * Module to provide user migration, 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 258badeb35e..622bc521952 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 @@ -7,11 +7,12 @@ import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { cleanupCollections, + importUserFactory, JwtTestFactory, schoolEntityFactory, systemEntityFactory, @@ -1342,5 +1343,65 @@ describe('UserLoginMigrationController (API)', () => { expect(response.body).toEqual({}); }); }); + + describe('when the migration wizard is also running', () => { + const setup = async () => { + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [sourceSystem], + officialSchoolNumber: '12345', + inUserMigration: true, + inMaintenanceSince: new Date(2024, 1, 4), + }); + const importUser = importUserFactory.build({ school }); + const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ + school, + targetSystem, + sourceSystem, + startedAt: new Date(2023, 1, 4), + }); + + const migratedUser: User = userFactory.buildWithId({ + lastLoginSystemChange: new Date(2023, 1, 5), + }); + + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); + + await em.persistAndFlush([ + sourceSystem, + targetSystem, + school, + adminAccount, + adminUser, + userLoginMigration, + migratedUser, + importUser, + ]); + em.clear(); + + const loggedInClient = await testApiClient.login(adminAccount); + + return { + loggedInClient, + userLoginMigration, + adminUser, + }; + }; + + it('should close migration wizard', async () => { + const { loggedInClient, adminUser } = await setup(); + + await loggedInClient.post('/close'); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [entities, count] = await em.findAndCount(ImportUser, {}); + expect(count).toEqual(0); + + const school = await em.findOneOrFail(SchoolEntity, { id: adminUser.school.id }); + expect(school.inUserMigration).toBe(undefined); + expect(school.inMaintenanceSince).toBe(undefined); + }); + }); }); }); 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 4ac3dc6f6dd..81019ee19a2 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 @@ -20,6 +20,7 @@ import { } from '../loggable'; import { UserLoginMigrationMapper } from '../mapper'; import { + CloseMigrationWizardUc, CloseUserLoginMigrationUc, RestartUserLoginMigrationUc, StartUserLoginMigrationUc, @@ -45,7 +46,8 @@ export class UserLoginMigrationController { private readonly startUserLoginMigrationUc: StartUserLoginMigrationUc, private readonly restartUserLoginMigrationUc: RestartUserLoginMigrationUc, private readonly toggleUserLoginMigrationUc: ToggleUserLoginMigrationUc, - private readonly closeUserLoginMigrationUc: CloseUserLoginMigrationUc + private readonly closeUserLoginMigrationUc: CloseUserLoginMigrationUc, + private readonly closeMigrationWizardUc: CloseMigrationWizardUc ) {} @Get() @@ -194,7 +196,7 @@ export class UserLoginMigrationController { description: 'User login migration does not exist', type: UserLoginMigrationNotFoundLoggableException, }) - @ApiOkResponse({ description: 'User login migration closed', type: UserLoginMigrationResponse }) + @ApiOkResponse({ description: 'User login migration and migration wizard closed', type: UserLoginMigrationResponse }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() @ApiNoContentResponse({ description: 'User login migration was reverted' }) @@ -205,10 +207,13 @@ export class UserLoginMigrationController { ); if (userLoginMigration) { + await this.closeMigrationWizardUc.closeMigrationWizardWhenActive(currentUser.userId); + const migrationResponse: UserLoginMigrationResponse = UserLoginMigrationMapper.mapUserLoginMigrationDoToResponse(userLoginMigration); return migrationResponse; } + return undefined; } diff --git a/apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.spec.ts new file mode 100644 index 00000000000..b8e7b9f589c --- /dev/null +++ b/apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.spec.ts @@ -0,0 +1,118 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserImportService } from '@modules/user-import'; +import { Test, TestingModule } from '@nestjs/testing'; +import { legacySchoolDoFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; +import { ObjectId } from 'bson'; +import { CloseMigrationWizardUc } from './close-migration-wizard.uc'; + +describe(CloseMigrationWizardUc.name, () => { + let module: TestingModule; + let uc: CloseMigrationWizardUc; + + let schoolService: DeepMocked; + let userImportService: DeepMocked; + let authorizationService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + CloseMigrationWizardUc, + { + provide: LegacySchoolService, + useValue: createMock(), + }, + { + provide: UserImportService, + useValue: createMock(), + }, + { + provide: AuthorizationService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(CloseMigrationWizardUc); + schoolService = module.get(LegacySchoolService); + userImportService = module.get(UserImportService); + authorizationService = module.get(AuthorizationService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('closeMigrationWizardWhenActive', () => { + describe('when school is in user migration', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + const user = userFactory.buildWithId({ school: schoolEntityFactory.build({ _id: schoolId }) }); + const school = legacySchoolDoFactory.build({ inUserMigration: true }); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + schoolService.getSchoolById.mockResolvedValueOnce(school); + + return { + user, + school, + }; + }; + + it('should check users permissions', async () => { + const { user } = setup(); + + await uc.closeMigrationWizardWhenActive(user.id); + + expect(authorizationService.checkAllPermissions).toHaveBeenCalledWith(user, ['IMPORT_USER_MIGRATE']); + }); + + it('should reset migration wizard for users school', async () => { + const { user, school } = setup(); + + await uc.closeMigrationWizardWhenActive(user.id); + + expect(userImportService.resetMigrationForUsersSchool).toHaveBeenCalledWith(user, school); + }); + }); + + describe('when school is not in user migration', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + const user = userFactory.buildWithId({ school: schoolEntityFactory.build({ _id: schoolId }) }); + const school = legacySchoolDoFactory.build({ inUserMigration: false }); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + schoolService.getSchoolById.mockResolvedValueOnce(school); + + return { + user, + school, + }; + }; + + it('should not check users permissions', async () => { + const { user } = setup(); + + await uc.closeMigrationWizardWhenActive(user.id); + + expect(authorizationService.checkAllPermissions).not.toHaveBeenCalled(); + }); + + it('should not reset migration wizard for users school', async () => { + const { user } = setup(); + + await uc.closeMigrationWizardWhenActive(user.id); + + expect(userImportService.resetMigrationForUsersSchool).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.ts b/apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.ts new file mode 100644 index 00000000000..4071b5a5400 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/uc/close-migration-wizard.uc.ts @@ -0,0 +1,28 @@ +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserImportService } from '@modules/user-import'; +import { Injectable } from '@nestjs/common'; +import { LegacySchoolDo } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; + +@Injectable() +export class CloseMigrationWizardUc { + constructor( + private readonly schoolService: LegacySchoolService, + private readonly userImportService: UserImportService, + private readonly authorizationService: AuthorizationService + ) {} + + public async closeMigrationWizardWhenActive(userId: EntityId): Promise { + const user: User = await this.authorizationService.getUserWithPermissions(userId); + const school: LegacySchoolDo = await this.schoolService.getSchoolById(user.school.id); + + if (school.inUserMigration) { + this.authorizationService.checkAllPermissions(user, [Permission.IMPORT_USER_MIGRATE]); + + await this.userImportService.resetMigrationForUsersSchool(user, school); + } + } +} 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 5228e644733..caabd374df0 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 @@ -16,7 +16,7 @@ export class CloseUserLoginMigrationUc { private readonly authorizationService: AuthorizationService ) {} - async closeMigration(userId: EntityId, schoolId: EntityId): Promise { + public async closeMigration(userId: EntityId, schoolId: EntityId): Promise { const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( schoolId ); diff --git a/apps/server/src/modules/user-login-migration/uc/index.ts b/apps/server/src/modules/user-login-migration/uc/index.ts index da6c995ded5..5a4d6eb5f7d 100644 --- a/apps/server/src/modules/user-login-migration/uc/index.ts +++ b/apps/server/src/modules/user-login-migration/uc/index.ts @@ -5,3 +5,4 @@ export * from './toggle-user-login-migration.uc'; export * from './restart-user-login-migration.uc'; export * from './close-user-login-migration.uc'; export * from './user-login-migration-rollback.uc'; +export { CloseMigrationWizardUc } from './close-migration-wizard.uc'; 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 21471537a76..cf2151e31eb 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 @@ -3,11 +3,13 @@ import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; import { OauthModule } from '@modules/oauth'; import { ProvisioningModule } from '@modules/provisioning'; +import { ImportUserModule } from '@modules/user-import'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { UserLoginMigrationRollbackController } from './controller/user-login-migration-rollback.controller'; import { UserLoginMigrationController } from './controller/user-login-migration.controller'; import { + CloseMigrationWizardUc, CloseUserLoginMigrationUc, RestartUserLoginMigrationUc, StartUserLoginMigrationUc, @@ -26,6 +28,7 @@ import { UserLoginMigrationModule } from './user-login-migration.module'; AuthorizationModule, LoggerModule, LegacySchoolModule, + ImportUserModule, ], providers: [ UserLoginMigrationUc, @@ -34,6 +37,7 @@ import { UserLoginMigrationModule } from './user-login-migration.module'; ToggleUserLoginMigrationUc, CloseUserLoginMigrationUc, UserLoginMigrationRollbackUc, + CloseMigrationWizardUc, ], controllers: [UserLoginMigrationController, UserLoginMigrationRollbackController], }) From b319e57cc83e481310efd52e9cc0306f67df43f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:23:00 +0200 Subject: [PATCH 03/35] N21-2023 Add validity period provisioning to groups (#5075) --- .../src/core/logger/types/logging.types.ts | 2 +- .../src/infra/schulconnex-client/index.ts | 17 +- .../schulconnex-client/response/index.ts | 1 + .../response/schulconnex-gruppe-response.ts | 10 +- .../response/schulconnex-laufzeit-response.ts | 21 ++ .../testing/schulconnex-response-factory.ts | 4 + .../provisioning/domain/error/index.ts | 2 + ...ufzeit-response.loggable-exception.spec.ts | 31 +++ ...id-laufzeit-response.loggable-exception.ts | 19 ++ ...eriode-response.loggable-exception.spec.ts | 30 +++ ...lernperiode-response.loggable-exception.ts | 18 ++ .../src/modules/provisioning/domain/index.ts | 1 + .../schulconnex-course-sync.service.spec.ts | 8 + .../schulconnex-course-sync.service.ts | 3 + .../strategy/sanis/sanis.strategy.ts | 4 +- .../sanis/schulconnex-response-mapper.spec.ts | 226 +++++++++++++++++- .../sanis/schulconnex-response-mapper.ts | 85 ++++++- 17 files changed, 452 insertions(+), 30 deletions(-) create mode 100644 apps/server/src/infra/schulconnex-client/response/schulconnex-laufzeit-response.ts create mode 100644 apps/server/src/modules/provisioning/domain/error/index.ts create mode 100644 apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.ts create mode 100644 apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.ts create mode 100644 apps/server/src/modules/provisioning/domain/index.ts diff --git a/apps/server/src/core/logger/types/logging.types.ts b/apps/server/src/core/logger/types/logging.types.ts index 5271ba85338..c5fcdc1f8c9 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 | boolean | undefined }; + data?: LogMessageDataObject; }; export type ValidationErrorLogMessage = { diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts index 8d08d2fa56f..bf68886a92a 100644 --- a/apps/server/src/infra/schulconnex-client/index.ts +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -1,21 +1,6 @@ export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; export { SchulconnexClientModule } from './schulconnex-client.module'; export { SchulconnexRestClient } from './schulconnex-rest-client'; -export { - SchulconnexResponse, - SchulconnexRole, - SchulconnexGroupRole, - SchulconnexGroupType, - SchulconnexGruppenResponse, - SchulconnexResponseValidationGroups, - SchulconnexPersonResponse, - SchulconnexAnschriftResponse, - SchulconnexGruppenzugehoerigkeitResponse, - SchulconnexGruppeResponse, - SchulconnexNameResponse, - SchulconnexOrganisationResponse, - SchulconnexPersonenkontextResponse, - SchulconnexSonstigeGruppenzugehoerigeResponse, -} from './response'; +export * from './response'; export { schulconnexResponseFactory, schulconnexLizenzInfoResponseFactory } from './testing'; export { SchulconnexClientConfig } from './schulconnex-client-config'; diff --git a/apps/server/src/infra/schulconnex-client/response/index.ts b/apps/server/src/infra/schulconnex-client/response/index.ts index fed85ea47fa..d453e030115 100644 --- a/apps/server/src/infra/schulconnex-client/response/index.ts +++ b/apps/server/src/infra/schulconnex-client/response/index.ts @@ -14,4 +14,5 @@ export { SchulconnexAnschriftResponse } from './schulconnex-anschrift-response'; export { SchulconnexResponseValidationGroups } from './schulconnex-response-validation-groups'; export { SchulconnexErreichbarkeitenResponse } from './schulconnex-erreichbarkeiten-response'; export { SchulconnexCommunicationType } from './schulconnex-communication-type'; +export { SchulconnexLaufzeitResponse, lernperiodeFormat } from './schulconnex-laufzeit-response'; export * from './lizenz-info'; diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts index 4a9a754b71a..2533d306743 100644 --- a/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts @@ -1,5 +1,7 @@ -import { IsEnum, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { IsEnum, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; import { SchulconnexGroupType } from './schulconnex-group-type'; +import { SchulconnexLaufzeitResponse } from './schulconnex-laufzeit-response'; export class SchulconnexGruppeResponse { @IsString() @@ -10,4 +12,10 @@ export class SchulconnexGruppeResponse { @IsEnum(SchulconnexGroupType) typ!: SchulconnexGroupType; + + @IsOptional() + @IsObject() + @ValidateNested() + @Type(() => SchulconnexLaufzeitResponse) + laufzeit?: SchulconnexLaufzeitResponse; } diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-laufzeit-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-laufzeit-response.ts new file mode 100644 index 00000000000..96f39c61a50 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-laufzeit-response.ts @@ -0,0 +1,21 @@ +import { IsDateString, IsOptional, Matches } from 'class-validator'; + +export const lernperiodeFormat = /^(\d{4})(?:-([1-2]))?$/; + +export class SchulconnexLaufzeitResponse { + @IsOptional() + @IsDateString() + von?: string; + + @IsOptional() + @IsDateString() + bis?: string; + + @IsOptional() + @Matches(lernperiodeFormat) + vonlernperiode?: string; + + @IsOptional() + @Matches(lernperiodeFormat) + bislernperiode?: string; +} diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts index 2cb7dcb5ece..c41e603e37e 100644 --- a/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts +++ b/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts @@ -38,6 +38,10 @@ export const schulconnexResponseFactory = Factory.define(() id: new UUID().toString(), bezeichnung: 'bezeichnung', typ: SchulconnexGroupType.CLASS, + laufzeit: { + vonlernperiode: '2024-1', + bislernperiode: '2024-2', + }, }, gruppenzugehoerigkeit: { rollen: [SchulconnexGroupRole.TEACHER], diff --git a/apps/server/src/modules/provisioning/domain/error/index.ts b/apps/server/src/modules/provisioning/domain/error/index.ts new file mode 100644 index 00000000000..ce58e5b2fdf --- /dev/null +++ b/apps/server/src/modules/provisioning/domain/error/index.ts @@ -0,0 +1,2 @@ +export { InvalidLernperiodeResponseLoggableException } from './invalid-lernperiode-response.loggable-exception'; +export { InvalidLaufzeitResponseLoggableException } from './invalid-laufzeit-response.loggable-exception'; diff --git a/apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.spec.ts b/apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.spec.ts new file mode 100644 index 00000000000..f8f29727c09 --- /dev/null +++ b/apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { SchulconnexLaufzeitResponse } from '@infra/schulconnex-client'; +import { InvalidLaufzeitResponseLoggableException } from './invalid-laufzeit-response.loggable-exception'; + +describe(InvalidLaufzeitResponseLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const laufzeit = new SchulconnexLaufzeitResponse(); + + const exception = new InvalidLaufzeitResponseLoggableException(laufzeit); + + return { + exception, + laufzeit, + }; + }; + + it('should return the correct log message', () => { + const { exception, laufzeit } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'INVALID_LAUFZEIT_RESPONSE', + stack: expect.any(String), + data: { + laufzeit, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.ts b/apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.ts new file mode 100644 index 00000000000..bb46677224e --- /dev/null +++ b/apps/server/src/modules/provisioning/domain/error/invalid-laufzeit-response.loggable-exception.ts @@ -0,0 +1,19 @@ +import { SchulconnexLaufzeitResponse } from '@infra/schulconnex-client'; +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; + +export class InvalidLaufzeitResponseLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly laufzeit: SchulconnexLaufzeitResponse) { + super(); + } + + getLogMessage(): ErrorLogMessage { + return { + type: 'INVALID_LAUFZEIT_RESPONSE', + stack: this.stack, + data: { + laufzeit: { ...this.laufzeit }, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.spec.ts b/apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.spec.ts new file mode 100644 index 00000000000..43721f356b6 --- /dev/null +++ b/apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.spec.ts @@ -0,0 +1,30 @@ +import { InvalidLernperiodeResponseLoggableException } from './invalid-lernperiode-response.loggable-exception'; + +describe(InvalidLernperiodeResponseLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const lernperiode = '2024-223'; + + const exception = new InvalidLernperiodeResponseLoggableException(lernperiode); + + return { + exception, + lernperiode, + }; + }; + + it('should return the correct log message', () => { + const { exception, lernperiode } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'INVALID_LERNPERIODE_RESPONSE', + stack: expect.any(String), + data: { + lernperiode, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.ts b/apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.ts new file mode 100644 index 00000000000..6138b24c7f1 --- /dev/null +++ b/apps/server/src/modules/provisioning/domain/error/invalid-lernperiode-response.loggable-exception.ts @@ -0,0 +1,18 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; + +export class InvalidLernperiodeResponseLoggableException extends InternalServerErrorException implements Loggable { + constructor(private readonly lernperiode: string) { + super(); + } + + getLogMessage(): ErrorLogMessage { + return { + type: 'INVALID_LERNPERIODE_RESPONSE', + stack: this.stack, + data: { + lernperiode: this.lernperiode, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/domain/index.ts b/apps/server/src/modules/provisioning/domain/index.ts new file mode 100644 index 00000000000..93ae819eac1 --- /dev/null +++ b/apps/server/src/modules/provisioning/domain/index.ts @@ -0,0 +1 @@ +export * from './error'; diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.spec.ts index a662a5d5502..881822cdea6 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.spec.ts @@ -88,6 +88,8 @@ describe(SchulconnexCourseSyncService.name, () => { new Course({ ...course.getProps(), name: newGroup.name, + startDate: newGroup.validFrom, + untilDate: newGroup.validUntil, studentIds: [studentId], teacherIds: [teacherId], }), @@ -126,6 +128,8 @@ describe(SchulconnexCourseSyncService.name, () => { new Course({ ...course.getProps(), name: newGroup.name, + startDate: newGroup.validFrom, + untilDate: newGroup.validUntil, studentIds: [], teacherIds: [], }), @@ -164,6 +168,8 @@ describe(SchulconnexCourseSyncService.name, () => { new Course({ ...course.getProps(), name: course.name, + startDate: newGroup.validFrom, + untilDate: newGroup.validUntil, studentIds: [], teacherIds: [], }), @@ -208,6 +214,8 @@ describe(SchulconnexCourseSyncService.name, () => { new Course({ ...course.getProps(), name: course.name, + startDate: newGroup.validFrom, + untilDate: newGroup.validUntil, studentIds: [], teacherIds: [teacherUserId], syncedWithGroup: undefined, diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.ts index bdc853f49a5..04049de68aa 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.ts @@ -22,6 +22,9 @@ export class SchulconnexCourseSyncService { course.name = newGroup.name; } + course.startDate = newGroup.validFrom; + course.untilDate = newGroup.validUntil; + const students: GroupUser[] = newGroup.users.filter( (user: GroupUser): boolean => user.roleId === studentRole.id ); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index b5ddfff8238..760f93218e1 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -44,9 +44,9 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { protected readonly groupService: GroupService, protected readonly schulconnexLicenseProvisioningService: SchulconnexLicenseProvisioningService, protected readonly schulconnexToolProvisioningService: SchulconnexToolProvisioningService, + protected readonly configService: ConfigService, private readonly responseMapper: SchulconnexResponseMapper, - private readonly schulconnexRestClient: SchulconnexRestClient, - protected readonly configService: ConfigService + private readonly schulconnexRestClient: SchulconnexRestClient ) { super( provisioningFeatures, diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts index 86a91085ec6..5a8bc7d0a15 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts @@ -3,18 +3,19 @@ import { SchulconnexGroupRole, SchulconnexGroupType, SchulconnexGruppenResponse, + SchulconnexLizenzInfoResponse, schulconnexLizenzInfoResponseFactory, SchulconnexPersonenkontextResponse, SchulconnexResponse, schulconnexResponseFactory, SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client'; -import { SchulconnexLizenzInfoResponse } from '@infra/schulconnex-client/response'; import { GroupTypes } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; +import { InvalidLaufzeitResponseLoggableException, InvalidLernperiodeResponseLoggableException } from '../../domain'; import { ExternalGroupDto, ExternalLicenseDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; @@ -147,6 +148,7 @@ describe(SchulconnexResponseMapper.name, () => { }); const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + const personenkontext: SchulconnexPersonenkontextResponse = schulconnexResponse.personenkontexte[0]; const group: SchulconnexGruppenResponse = personenkontext.gruppen![0]; const otherParticipant: SchulconnexSonstigeGruppenzugehoerigeResponse = group.sonstige_gruppenzugehoerige![0]; @@ -178,6 +180,8 @@ describe(SchulconnexResponseMapper.name, () => { roleName: RoleName.STUDENT, }, ], + from: new Date('2024-08-01'), + until: new Date('2025-07-31'), }); }); }); @@ -336,6 +340,226 @@ describe(SchulconnexResponseMapper.name, () => { expect(result?.[0].otherUsers).toHaveLength(0); }); }); + + describe('when the group has no duration', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = undefined; + + return { + schulconnexResponse, + }; + }; + + it('should map the group without a duration', () => { + const { schulconnexResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(schulconnexResponse); + + expect(result).toEqual([ + expect.objectContaining>({ + from: undefined, + until: undefined, + }), + ]); + }); + }); + + describe('when the group has a duration as lernperiode', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = { + vonlernperiode: '2023-2', + bislernperiode: '2026-1', + }; + + return { + schulconnexResponse, + }; + }; + + it('should map the group a duration', () => { + const { schulconnexResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(schulconnexResponse); + + expect(result).toEqual([ + expect.objectContaining>({ + from: new Date('2024-02-01'), + until: new Date('2027-01-31'), + }), + ]); + }); + }); + + describe('when the group has a duration as an exact date', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + const duration = { + von: '2024-05-13', + bis: '2028-07-12', + }; + + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = duration; + + return { + schulconnexResponse, + duration, + }; + }; + + it('should map the group with an exact date', () => { + const { schulconnexResponse, duration } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(schulconnexResponse); + + expect(result).toEqual([ + expect.objectContaining>({ + from: new Date(duration.von), + until: new Date(duration.bis), + }), + ]); + }); + }); + + describe('when the group has a duration as an exact date and as lernperiode', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + const duration = { + von: '2024-05-13', + bis: '2028-07-12', + vonlernperiode: '2024', + bislernperiode: '2025', + }; + + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = duration; + + return { + schulconnexResponse, + duration, + }; + }; + + it('should map the group with an exact date', () => { + const { schulconnexResponse, duration } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(schulconnexResponse); + + expect(result).toEqual([ + expect.objectContaining>({ + from: new Date(duration.von), + until: new Date(duration.bis), + }), + ]); + }); + }); + + describe('when the group has an invalid duration as lernperiode', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = { + vonlernperiode: '2024-3', + bislernperiode: '2021-01-02', + }; + + return { + schulconnexResponse, + }; + }; + + it('should throw an error', () => { + const { schulconnexResponse } = setup(); + + expect(() => mapper.mapToExternalGroupDtos(schulconnexResponse)).toThrow( + InvalidLernperiodeResponseLoggableException + ); + }); + }); + + describe('when the group has no from date', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = { + bislernperiode: '2024-2', + }; + + return { + schulconnexResponse, + }; + }; + + it('should throw an error', () => { + const { schulconnexResponse } = setup(); + + expect(() => mapper.mapToExternalGroupDtos(schulconnexResponse)).toThrow( + InvalidLaufzeitResponseLoggableException + ); + }); + }); + + describe('when the group has no until date', () => { + const setup = () => { + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.laufzeit = { + vonlernperiode: '2024-2', + }; + + return { + schulconnexResponse, + }; + }; + + it('should throw an error', () => { + const { schulconnexResponse } = setup(); + + expect(() => mapper.mapToExternalGroupDtos(schulconnexResponse)).toThrow( + InvalidLaufzeitResponseLoggableException + ); + }); + }); + }); + + describe('mapLernperiode', () => { + describe('when the lernperiode is a full year', () => { + it('should map the correct date', () => { + const result = SchulconnexResponseMapper.mapLernperiode('2024'); + + expect(result).toEqual({ + from: new Date('2024-08-01'), + until: new Date('2025-07-31'), + }); + }); + }); + + describe('when the lernperiode is the first half year', () => { + it('should map the correct date', () => { + const result = SchulconnexResponseMapper.mapLernperiode('2024-1'); + + expect(result).toEqual({ + from: new Date('2024-08-01'), + until: new Date('2025-01-31'), + }); + }); + }); + + describe('when the lernperiode is the second half year', () => { + it('should map the correct date', () => { + const result = SchulconnexResponseMapper.mapLernperiode('2024-2'); + + expect(result).toEqual({ + from: new Date('2025-02-01'), + until: new Date('2025-07-31'), + }); + }); + }); + + describe('when the lernperiode is invalid', () => { + it('should throw an error', () => { + expect(() => SchulconnexResponseMapper.mapLernperiode('2024-3')).toThrow( + InvalidLernperiodeResponseLoggableException + ); + }); + }); }); describe('mapToExternalLicenses', () => { diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts index aef7f09366d..7e66f6d2900 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts @@ -1,21 +1,22 @@ import { - SchulconnexGruppenResponse, - SchulconnexResponse, - SchulconnexSonstigeGruppenzugehoerigeResponse, -} from '@infra/schulconnex-client'; -import { + lernperiodeFormat, SchulconnexCommunicationType, SchulconnexErreichbarkeitenResponse, + SchulconnexGroupRole, + SchulconnexGroupType, + SchulconnexGruppenResponse, + SchulconnexLaufzeitResponse, SchulconnexLizenzInfoResponse, + SchulconnexResponse, + SchulconnexRole, + SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client/response'; -import { SchulconnexGroupRole } from '@infra/schulconnex-client/response/schulconnex-group-role'; -import { SchulconnexGroupType } from '@infra/schulconnex-client/response/schulconnex-group-type'; -import { SchulconnexRole } from '@infra/schulconnex-client/response/schulconnex-role'; import { GroupTypes } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; +import { InvalidLaufzeitResponseLoggableException, InvalidLernperiodeResponseLoggableException } from '../../domain'; import { ExternalGroupDto, ExternalGroupUserDto, @@ -43,6 +44,11 @@ const GroupTypeMapping: Partial> = { [SchulconnexGroupType.OTHER]: GroupTypes.OTHER, }; +type TimePeriode = { + from: Date; + until: Date; +}; + @Injectable() export class SchulconnexResponseMapper { SCHOOLNUMBER_PREFIX_REGEX = /^NI_/; @@ -143,13 +149,19 @@ export class SchulconnexResponseMapper { : []; } - return new ExternalGroupDto({ + const groupDuration: TimePeriode | undefined = SchulconnexResponseMapper.mapGroupDuration(group.gruppe.laufzeit); + + const externalGroup: ExternalGroupDto = new ExternalGroupDto({ name: group.gruppe.bezeichnung, type: groupType, externalId: group.gruppe.id, user, otherUsers, + from: groupDuration?.from, + until: groupDuration?.until, }); + + return externalGroup; } private mapToExternalGroupUser(relation: SchulconnexSonstigeGruppenzugehoerigeResponse): ExternalGroupUserDto | null { @@ -172,6 +184,61 @@ export class SchulconnexResponseMapper { return mapped; } + private static mapGroupDuration(duration: SchulconnexLaufzeitResponse | undefined): TimePeriode | undefined { + if (!duration) { + return undefined; + } + + let from: Date; + let until: Date; + if (duration.von) { + from = new Date(duration.von); + } else if (duration.vonlernperiode) { + const fromPeriode: TimePeriode = SchulconnexResponseMapper.mapLernperiode(duration.vonlernperiode); + + from = fromPeriode.from; + } else { + throw new InvalidLaufzeitResponseLoggableException(duration); + } + + if (duration.bis) { + until = new Date(duration.bis); + } else if (duration.bislernperiode) { + const untilPeriode: TimePeriode = SchulconnexResponseMapper.mapLernperiode(duration.bislernperiode); + + until = untilPeriode.until; + } else { + throw new InvalidLaufzeitResponseLoggableException(duration); + } + + return { + from, + until, + }; + } + + public static mapLernperiode(lernperiode: string): TimePeriode { + const matches: RegExpMatchArray | null = lernperiode.match(lernperiodeFormat); + + if (!matches || matches.length < 2) { + throw new InvalidLernperiodeResponseLoggableException(lernperiode); + } + + const year = Number(matches[1]); + const semester: number = matches.length >= 3 ? Number(matches[2]) : 0; + + const startMonth: string = semester === 2 ? '02' : '08'; + const endMonth: string = semester === 1 ? '01' : '07'; + + const startYear: number = semester === 2 ? year + 1 : year; + const endYear: number = year + 1; + + return { + from: new Date(`${startYear}-${startMonth}-01`), + until: new Date(`${endYear}-${endMonth}-31`), + }; + } + public static mapToExternalLicenses(licenseInfos: SchulconnexLizenzInfoResponse[]): ExternalLicenseDto[] { const externalLicenseDtos: ExternalLicenseDto[] = licenseInfos .map((license: SchulconnexLizenzInfoResponse) => { From 4bffe541e079e02ff7c55977d976e849c4db0e81 Mon Sep 17 00:00:00 2001 From: Majed Mak <132336669+MajedAlaitwniCap@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:41:32 +0200 Subject: [PATCH 04/35] EW-893 Reimplement isUniqueEmail validation (#5056) * Implmenting email check for the IDM service --- apps/server/src/apps/server.app.ts | 3 - .../modules/account/account.module.spec.ts | 6 - .../src/modules/account/account.module.ts | 4 +- .../modules/account/api/account.uc.spec.ts | 5 - .../account/api/test/account.api.spec.ts | 11 +- .../services/account-db.service.spec.ts | 45 +- .../domain/services/account-db.service.ts | 14 +- .../services/account-idm.service.spec.ts | 41 +- .../domain/services/account-idm.service.ts | 7 + .../services/account.service.abstract.ts | 2 + .../account.service.integration.spec.ts | 13 +- .../domain/services/account.service.spec.ts | 86 +- .../domain/services/account.service.ts | 25 +- .../account.validation.service.spec.ts | 430 ------ .../services/account.validation.service.ts | 36 - .../account.repo.integration.spec.ts | 35 +- .../account/repo/micro-orm/account.repo.ts | 14 +- .../account/testing/account.factory.ts | 6 +- src/services/user/firstLogin.js | 8 - src/services/user/hooks/userService.js | 1262 ++++++++--------- .../user/hooks/userService.hooks.test.js | 28 +- test/utils/setup.nest.services.js | 5 - 22 files changed, 880 insertions(+), 1206 deletions(-) delete mode 100644 apps/server/src/modules/account/domain/services/account.validation.service.spec.ts delete mode 100644 apps/server/src/modules/account/domain/services/account.validation.service.ts diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 94af5defccc..0718e15a6e4 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -4,7 +4,6 @@ import { Mail, MailService } from '@infra/mail'; /* eslint-disable no-console */ import { MikroORM } from '@mikro-orm/core'; import { AccountService } from '@modules/account'; -import { AccountValidationService } from '@src/modules/account/domain/services/account.validation.service'; import { AccountUc } from '@src/modules/account/api/account.uc'; import { SystemRule } from '@modules/authorization/domain/rules'; import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; @@ -83,8 +82,6 @@ async function bootstrap() { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-account-service'] = nestApp.get(AccountService); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - feathersExpress.services['nest-account-validation-service'] = nestApp.get(AccountValidationService); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-account-uc'] = nestApp.get(AccountUc); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-collaborative-storage-uc'] = nestApp.get(CollaborativeStorageUc); diff --git a/apps/server/src/modules/account/account.module.spec.ts b/apps/server/src/modules/account/account.module.spec.ts index ba7e80ec5c0..31a431eaf36 100644 --- a/apps/server/src/modules/account/account.module.spec.ts +++ b/apps/server/src/modules/account/account.module.spec.ts @@ -4,7 +4,6 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { AccountModule } from './account.module'; import { AccountIdmToDoMapper, AccountIdmToDoMapperDb, AccountIdmToDoMapperIdm } from './repo/micro-orm/mapper'; import { AccountService } from './domain/services/account.service'; -import { AccountValidationService } from './domain/services/account.validation.service'; describe('AccountModule', () => { let module: TestingModule; @@ -32,11 +31,6 @@ describe('AccountModule', () => { expect(accountService).toBeDefined(); }); - it('should have the account validation service defined', () => { - const accountValidationService = module.get(AccountValidationService); - expect(accountValidationService).toBeDefined(); - }); - describe('when FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED is enabled', () => { let moduleFeatureEnabled: TestingModule; diff --git a/apps/server/src/modules/account/account.module.ts b/apps/server/src/modules/account/account.module.ts index 4f7bd675c42..0f77a40fafd 100644 --- a/apps/server/src/modules/account/account.module.ts +++ b/apps/server/src/modules/account/account.module.ts @@ -11,7 +11,6 @@ import { AccountIdmToDoMapper, AccountIdmToDoMapperDb, AccountIdmToDoMapperIdm } import { AccountServiceDb } from './domain/services/account-db.service'; import { AccountServiceIdm } from './domain/services/account-idm.service'; import { AccountService } from './domain/services/account.service'; -import { AccountValidationService } from './domain/services/account.validation.service'; function accountIdmToDtoMapperFactory(configService: ConfigService): AccountIdmToDoMapper { if (configService.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') === true) { @@ -29,13 +28,12 @@ function accountIdmToDtoMapperFactory(configService: ConfigService { provide: ConfigService, useValue: createMock(), }, - { - provide: AccountValidationService, - useValue: createMock(), - }, { provide: AuthorizationService, useValue: createMock(), diff --git a/apps/server/src/modules/account/api/test/account.api.spec.ts b/apps/server/src/modules/account/api/test/account.api.spec.ts index e5ee1b94c2d..417e6a3a26b 100644 --- a/apps/server/src/modules/account/api/test/account.api.spec.ts +++ b/apps/server/src/modules/account/api/test/account.api.spec.ts @@ -1,10 +1,13 @@ +import { faker } from '@faker-js/faker'; import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { User } from '@shared/domain/entity'; import { Permission, RoleName } from '@shared/domain/interface'; import { TestApiClient, cleanupCollections, roleFactory, schoolEntityFactory, userFactory } from '@shared/testing'; -import { ServerTestModule } from '@modules/server/server.module'; +import { AccountEntity } from '../../domain/entity/account.entity'; +import { accountFactory } from '../../testing'; import { AccountByIdBodyParams, AccountSearchQueryParams, @@ -12,8 +15,6 @@ import { PatchMyAccountParams, PatchMyPasswordParams, } from '../dto'; -import { AccountEntity } from '../../domain/entity/account.entity'; -import { accountFactory } from '../../testing'; describe('Account Controller (API)', () => { const basePath = '/account'; @@ -495,8 +496,8 @@ describe('Account Controller (API)', () => { const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const studentAccount = mapUserToAccount(studentUser); - const superheroAccount = mapUserToAccount(superheroUser); + const studentAccount = accountFactory.withUser(studentUser).build({ username: faker.internet.email() }); + const superheroAccount = accountFactory.withUser(superheroUser).build(); em.persist(school); em.persist([studentRoles, superheroRoles]); diff --git a/apps/server/src/modules/account/domain/services/account-db.service.spec.ts b/apps/server/src/modules/account/domain/services/account-db.service.spec.ts index e39962db20b..5ea9543837a 100644 --- a/apps/server/src/modules/account/domain/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account-db.service.spec.ts @@ -1,3 +1,4 @@ +import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { IdentityManagementService } from '@infra/identity-management'; import { ObjectId } from '@mikro-orm/mongodb'; @@ -10,12 +11,12 @@ import { setupEntities, userFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import bcrypt from 'bcryptjs'; import { v1 } from 'uuid'; -import { Account } from '../account'; import { AccountConfig } from '../../account-config'; import { AccountRepo } from '../../repo/micro-orm/account.repo'; +import { accountDoFactory } from '../../testing'; +import { Account } from '../account'; import { AccountEntity } from '../entity/account.entity'; import { AccountServiceDb } from './account-db.service'; -import { accountDoFactory } from '../../testing'; describe('AccountDbService', () => { let module: TestingModule; @@ -921,6 +922,7 @@ describe('AccountDbService', () => { }); }); }); + describe('findMany', () => { describe('when find many one time', () => { const setup = () => { @@ -955,4 +957,43 @@ describe('AccountDbService', () => { }); }); }); + + describe('isUniqueEmail', () => { + describe('when email is unique', () => { + const setup = () => { + const email = faker.internet.email(); + + accountRepo.findByUsername.mockResolvedValue(null); + + return { email }; + }; + + it('should return true', async () => { + const { email } = setup(); + + const result = await accountService.isUniqueEmail(email); + + expect(result).toBe(true); + }); + }); + + describe('when email is not unique', () => { + const setup = () => { + const email = faker.internet.email(); + const mockTeacherAccount = accountDoFactory.build(); + + accountRepo.findByUsername.mockResolvedValue(mockTeacherAccount); + + return { email, mockTeacherAccount }; + }; + + it('should return false', async () => { + const { email } = setup(); + + const result = await accountService.isUniqueEmail(email); + + expect(result).toBe(false); + }); + }); + }); }); diff --git a/apps/server/src/modules/account/domain/services/account-db.service.ts b/apps/server/src/modules/account/domain/services/account-db.service.ts index 0a2d9b69de8..b95ddafb1f0 100644 --- a/apps/server/src/modules/account/domain/services/account-db.service.ts +++ b/apps/server/src/modules/account/domain/services/account-db.service.ts @@ -9,14 +9,17 @@ import { AccountConfig } from '../../account-config'; import { AccountRepo } from '../../repo/micro-orm/account.repo'; import { Account } from '../account'; import { AccountSave } from '../account-save'; +import { AbstractAccountService } from './account.service.abstract'; @Injectable() -export class AccountServiceDb { +export class AccountServiceDb extends AbstractAccountService { constructor( private readonly accountRepo: AccountRepo, private readonly idmService: IdentityManagementService, private readonly configService: ConfigService - ) {} + ) { + super(); + } async findById(id: EntityId): Promise { const internalId = await this.getInternalId(id); @@ -142,4 +145,11 @@ export class AccountServiceDb { return account; } + + public async isUniqueEmail(email: string): Promise { + const account = await this.accountRepo.findByUsername(email); + const isUnique = !account; + + return isUnique; + } } diff --git a/apps/server/src/modules/account/domain/services/account-idm.service.spec.ts b/apps/server/src/modules/account/domain/services/account-idm.service.spec.ts index 734143b331c..492b3269bf5 100644 --- a/apps/server/src/modules/account/domain/services/account-idm.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account-idm.service.spec.ts @@ -1,3 +1,4 @@ +import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { IdentityManagementOauthService, IdentityManagementService } from '@infra/identity-management'; @@ -7,8 +8,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { IdmAccount } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; -import { AccountConfig } from '../../account-config'; import { Account, AccountSave } from '..'; +import { AccountConfig } from '../../account-config'; import { AccountIdmToDoMapper, AccountIdmToDoMapperDb } from '../../repo/micro-orm/mapper'; import { AccountServiceIdm } from './account-idm.service'; @@ -532,4 +533,42 @@ describe('AccountIdmService', () => { await expect(accountIdmService.findMany(0, 0)).rejects.toThrow(NotImplementedException); }); }); + + describe('isUniqueEmail', () => { + describe('when email is unique', () => { + const setup = () => { + const email = faker.internet.email(); + + idmServiceMock.findAccountsByUsername.mockResolvedValue([[], 0]); + + return { email }; + }; + + it('should return true', async () => { + const { email } = setup(); + + const result = await accountIdmService.isUniqueEmail(email); + + expect(result).toBe(true); + }); + }); + + describe('when email is not unique', () => { + const setup = () => { + const email = faker.internet.email(); + + idmServiceMock.findAccountsByUsername.mockResolvedValue([[mockIdmAccount], 1]); + + return { email }; + }; + + it('should return false', async () => { + const { email } = setup(); + + const result = await accountIdmService.isUniqueEmail(email); + + expect(result).toBe(false); + }); + }); + }); }); diff --git a/apps/server/src/modules/account/domain/services/account-idm.service.ts b/apps/server/src/modules/account/domain/services/account-idm.service.ts index 766a159b0cb..c12fb5c7b9b 100644 --- a/apps/server/src/modules/account/domain/services/account-idm.service.ts +++ b/apps/server/src/modules/account/domain/services/account-idm.service.ts @@ -191,4 +191,11 @@ export class AccountServiceIdm extends AbstractAccountService { } throw new EntityNotFoundError(`Account with id ${id.toString()} not found`); } + + public async isUniqueEmail(email: string): Promise { + const [, count] = await this.identityManager.findAccountsByUsername(email); + const isUniqueEmail = count === 0; + + return isUniqueEmail; + } } diff --git a/apps/server/src/modules/account/domain/services/account.service.abstract.ts b/apps/server/src/modules/account/domain/services/account.service.abstract.ts index 5876136170d..80a48cd718b 100644 --- a/apps/server/src/modules/account/domain/services/account.service.abstract.ts +++ b/apps/server/src/modules/account/domain/services/account.service.abstract.ts @@ -46,4 +46,6 @@ export abstract class AbstractAccountService { abstract deleteByUserId(userId: EntityId): Promise; abstract searchByUsernameExactMatch(userName: string): Promise>; + + abstract isUniqueEmail(email: string): Promise; } diff --git a/apps/server/src/modules/account/domain/services/account.service.integration.spec.ts b/apps/server/src/modules/account/domain/services/account.service.integration.spec.ts index 899f648cad3..9d75d0a0308 100644 --- a/apps/server/src/modules/account/domain/services/account.service.integration.spec.ts +++ b/apps/server/src/modules/account/domain/services/account.service.integration.spec.ts @@ -10,18 +10,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IdmAccount } from '@shared/domain/interface'; import { UserRepo } from '@shared/repo'; import { cleanupCollections } from '@shared/testing'; -import { v1 } from 'uuid'; import { Logger } from '@src/core/logger'; +import { KeycloakIdentityManagementService } from '@src/infra/identity-management/keycloak/service/keycloak-identity-management.service'; +import { v1 } from 'uuid'; import { Account, AccountSave } from '..'; -import { AccountEntity } from '../entity/account.entity'; import { AccountRepo } from '../../repo/micro-orm/account.repo'; import { AccountIdmToDoMapper, AccountIdmToDoMapperDb } from '../../repo/micro-orm/mapper'; +import { accountFactory } from '../../testing'; +import { AccountEntity } from '../entity/account.entity'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; import { AccountService } from './account.service'; import { AbstractAccountService } from './account.service.abstract'; -import { AccountValidationService } from './account.validation.service'; -import { accountFactory } from '../../testing'; describe('AccountService Integration', () => { let module: TestingModule; @@ -93,7 +93,10 @@ describe('AccountService Integration', () => { AccountServiceDb, AccountRepo, UserRepo, - AccountValidationService, + { + provide: KeycloakIdentityManagementService, + useValue: createMock(), + }, { provide: AccountIdmToDoMapper, useValue: new AccountIdmToDoMapperDb(), diff --git a/apps/server/src/modules/account/domain/services/account.service.spec.ts b/apps/server/src/modules/account/domain/services/account.service.spec.ts index 4c4b4866776..cd2cbeea0e7 100644 --- a/apps/server/src/modules/account/domain/services/account.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account.service.spec.ts @@ -1,3 +1,4 @@ +import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MikroORM } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; @@ -28,14 +29,13 @@ import { IdmCallbackLoggableException } from '../error'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; import { AccountService } from './account.service'; -import { AccountValidationService } from './account.validation.service'; +import { AbstractAccountService } from './account.service.abstract'; describe('AccountService', () => { let module: TestingModule; let accountService: AccountService; let accountServiceIdm: DeepMocked; let accountServiceDb: DeepMocked; - let accountValidationService: DeepMocked; let configService: DeepMocked; let logger: DeepMocked; let userRepo: DeepMocked; @@ -48,7 +48,6 @@ describe('AccountService', () => { accountServiceDb, accountServiceIdm, configService, - accountValidationService, logger, userRepo, accountRepo, @@ -90,12 +89,6 @@ describe('AccountService', () => { provide: AccountRepo, useValue: createMock(), }, - { - provide: AccountValidationService, - useValue: { - isUniqueEmail: jest.fn().mockResolvedValue(true), - }, - }, { provide: UserRepo, useValue: createMock(), @@ -115,7 +108,6 @@ describe('AccountService', () => { accountServiceDb = module.get(AccountServiceDb); accountServiceIdm = module.get(AccountServiceIdm); accountService = module.get(AccountService); - accountValidationService = module.get(AccountValidationService); configService = module.get(ConfigService); logger = module.get(Logger); userRepo = module.get(UserRepo); @@ -366,7 +358,7 @@ describe('AccountService', () => { return { id: '' }; }, } as Account); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(true); return spy; }; @@ -405,7 +397,7 @@ describe('AccountService', () => { return { id: '' }; }, } as Account); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(true); }; it('should not throw an error', async () => { @@ -425,7 +417,7 @@ describe('AccountService', () => { return { id: '' }; }, } as Account); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(true); }; it('should not throw an error', async () => { @@ -459,9 +451,26 @@ describe('AccountService', () => { }); }); - describe('When username already exists', () => { + describe('When username already exists in mongoDB', () => { + const setup = () => { + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(false); + }; + + it('should throw username already exists', async () => { + setup(); + const params: AccountSave = { + username: 'john.doe@mail.tld', + password: 'JohnsPassword_123', + } as AccountSave; + await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username already exists'); + }); + }); + + describe('When username already exists in identity management', () => { const setup = () => { - accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); + configService.get.mockReturnValue(true); + + accountServiceIdm.isUniqueEmail.mockResolvedValueOnce(false); }; it('should throw username already exists', async () => { @@ -473,6 +482,7 @@ describe('AccountService', () => { await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username already exists'); }); }); + describe('When identity management is primary', () => { const setup = () => { configService.get.mockReturnValue(true); @@ -485,7 +495,7 @@ describe('AccountService', () => { accountServiceDb.save.mockResolvedValueOnce(account); accountServiceIdm.save.mockResolvedValueOnce(account); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceIdm.isUniqueEmail.mockResolvedValueOnce(true); return { service: newAccountService(), account }; }; @@ -1052,7 +1062,7 @@ describe('AccountService', () => { accountServiceDb.save.mockResolvedValue(mockStudentAccountDo); const spyAccountServiceSave = jest.spyOn(accountServiceDb, 'save'); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo, spyAccountServiceSave }; }; @@ -1088,7 +1098,7 @@ describe('AccountService', () => { accountServiceDb.validatePassword.mockResolvedValue(true); accountServiceDb.save.mockResolvedValue(mockStudentAccountDo); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1120,7 +1130,7 @@ describe('AccountService', () => { accountServiceDb.validatePassword.mockResolvedValue(true); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(true); accountServiceDb.save.mockResolvedValue(mockStudentAccountDo); const accountSaveSpy = jest.spyOn(accountServiceDb, 'save'); @@ -1157,7 +1167,7 @@ describe('AccountService', () => { accountServiceDb.validatePassword.mockResolvedValue(true); accountServiceDb.save.mockResolvedValue(mockStudentAccountDo); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(true); const userUpdateSpy = jest.spyOn(userRepo, 'save'); @@ -1192,7 +1202,7 @@ describe('AccountService', () => { accountServiceDb.validatePassword.mockResolvedValue(true); accountServiceDb.save.mockResolvedValue(mockStudentAccountDo); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(true); const accountSaveSpy = jest.spyOn(accountServiceDb, 'save'); const userUpdateSpy = jest.spyOn(userRepo, 'save'); @@ -1228,7 +1238,7 @@ describe('AccountService', () => { const mockStudentAccountDo: Account = AccountEntityToDoMapper.mapToDo(mockStudentAccount); accountServiceDb.validatePassword.mockResolvedValue(true); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(false); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1328,7 +1338,7 @@ describe('AccountService', () => { accountServiceDb.validatePassword.mockResolvedValue(true); accountServiceDb.save.mockRejectedValueOnce(undefined); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1361,7 +1371,7 @@ describe('AccountService', () => { accountServiceDb.validatePassword.mockResolvedValue(true); accountServiceDb.save.mockRejectedValueOnce(new ValidationError('fail to update')); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1431,7 +1441,7 @@ describe('AccountService', () => { Object.assign(mockStudentAccount, account); return Promise.resolve(AccountEntityToDoMapper.mapToDo(mockStudentAccount)); }); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1493,7 +1503,7 @@ describe('AccountService', () => { userRepo.save.mockResolvedValue(); accountServiceDb.save.mockRejectedValueOnce(undefined); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1522,7 +1532,7 @@ describe('AccountService', () => { userRepo.save.mockRejectedValueOnce(undefined); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + accountServiceDb.isUniqueEmail.mockResolvedValue(true); return { mockStudentUser, mockStudentAccountDo }; }; @@ -1583,7 +1593,7 @@ describe('AccountService', () => { const mockStudentAccountDo: Account = AccountEntityToDoMapper.mapToDo(mockStudentAccount); userRepo.save.mockRejectedValueOnce(undefined); - accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); + accountServiceDb.isUniqueEmail.mockResolvedValueOnce(false); return { mockStudentUser, mockStudentAccountDo, mockOtherTeacherAccount }; }; @@ -2073,4 +2083,24 @@ describe('AccountService', () => { }); }); }); + + describe('isUniqueEmail', () => { + describe('when checking if email is unique', () => { + const setup = () => { + const email = faker.internet.email(); + const accountImpl = Reflect.get(accountService, 'accountImpl') as DeepMocked; + const isUniqueEmailSpy = jest.spyOn(accountImpl, 'isUniqueEmail'); + + return { email, isUniqueEmailSpy }; + }; + + it('should call the underlying account service implementation', async () => { + const { email, isUniqueEmailSpy } = setup(); + + await accountService.isUniqueEmail(email); + + expect(isUniqueEmailSpy).toHaveBeenCalledWith(email); + }); + }); + }); }); diff --git a/apps/server/src/modules/account/domain/services/account.service.ts b/apps/server/src/modules/account/domain/services/account.service.ts index 37e6a6c8a81..042ab5a0f78 100644 --- a/apps/server/src/modules/account/domain/services/account.service.ts +++ b/apps/server/src/modules/account/domain/services/account.service.ts @@ -43,7 +43,6 @@ import { import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; import { AbstractAccountService } from './account.service.abstract'; -import { AccountValidationService } from './account.validation.service'; type UserPreferences = { firstLogin: boolean; @@ -58,7 +57,6 @@ export class AccountService extends AbstractAccountService implements DeletionSe private readonly accountDb: AccountServiceDb, private readonly accountIdm: AccountServiceIdm, private readonly configService: ConfigService, - private readonly accountValidationService: AccountValidationService, private readonly logger: Logger, private readonly userRepo: UserRepo, private readonly accountRepo: AccountRepo, @@ -123,7 +121,7 @@ export class AccountService extends AbstractAccountService implements DeletionSe ): Promise { if (updateData.email && user.email !== updateData.email) { const newMail = updateData.email.toLowerCase(); - await this.checkUniqueEmail(account, user, newMail); + await this.checkUniqueEmail(newMail); user.email = newMail; accountSave.username = newMail; return true; @@ -168,7 +166,7 @@ export class AccountService extends AbstractAccountService implements DeletionSe } if (updateData.username !== undefined) { const newMail = updateData.username.toLowerCase(); - await this.checkUniqueEmail(targetAccount, targetUser, newMail); + await this.checkUniqueEmail(newMail); targetUser.email = newMail; targetAccount.username = newMail; updateUser = true; @@ -324,14 +322,7 @@ export class AccountService extends AbstractAccountService implements DeletionSe // trimPassword hook will be done by class-validator ✔ // local.hooks.hashPassword('password'), will be done by account service ✔ // checkUnique ✔ - if ( - !(await this.accountValidationService.isUniqueEmail( - accountSave.username, - accountSave.userId, - accountSave.id, - accountSave.systemId - )) - ) { + if (!(await this.isUniqueEmail(accountSave.username))) { throw new ValidationError('Username already exists'); } // removePassword hook is not implemented @@ -435,8 +426,8 @@ export class AccountService extends AbstractAccountService implements DeletionSe return null; } - private async checkUniqueEmail(account: Account, user: User, email: string): Promise { - if (!(await this.accountValidationService.isUniqueEmail(email, user.id, account.id, account.systemId))) { + private async checkUniqueEmail(email: string): Promise { + if (!(await this.isUniqueEmail(email))) { throw new ValidationError(`The email address is already in use!`); } } @@ -446,4 +437,10 @@ export class AccountService extends AbstractAccountService implements DeletionSe return foundAccounts; } + + public async isUniqueEmail(email: string): Promise { + const isUniqueEmail = await this.accountImpl.isUniqueEmail(email); + + return isUniqueEmail; + } } diff --git a/apps/server/src/modules/account/domain/services/account.validation.service.spec.ts b/apps/server/src/modules/account/domain/services/account.validation.service.spec.ts deleted file mode 100644 index d376b8c81e3..00000000000 --- a/apps/server/src/modules/account/domain/services/account.validation.service.spec.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Role } from '@shared/domain/entity'; -import { Permission, RoleName } from '@shared/domain/interface'; -import { UserRepo } from '@shared/repo'; -import { setupEntities, systemFactory, userFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { AccountRepo } from '../../repo/micro-orm/account.repo'; -import { AccountValidationService } from './account.validation.service'; -import { accountDoFactory } from '../../testing'; - -describe('AccountValidationService', () => { - let module: TestingModule; - let accountValidationService: AccountValidationService; - - let userRepo: DeepMocked; - let accountRepo: DeepMocked; - - afterAll(async () => { - await module.close(); - }); - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - AccountValidationService, - { - provide: AccountRepo, - useValue: createMock(), - }, - { - provide: UserRepo, - useValue: createMock(), - }, - ], - }).compile(); - - accountValidationService = module.get(AccountValidationService); - - userRepo = module.get(UserRepo); - accountRepo = module.get(AccountRepo); - - await setupEntities(); - }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('isUniqueEmail', () => { - describe('When new email is available', () => { - const setup = () => { - userRepo.findByEmail.mockResolvedValueOnce([]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); - }; - it('should return true', async () => { - setup(); - - const res = await accountValidationService.isUniqueEmail('an@available.email'); - expect(res).toBe(true); - }); - }); - - describe('When new email is available', () => { - const setup = () => { - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); - - return { mockStudentUser }; - }; - it('should return true and ignore current user', async () => { - const { mockStudentUser } = setup(); - const res = await accountValidationService.isUniqueEmail(mockStudentUser.email, mockStudentUser.id); - expect(res).toBe(true); - }); - }); - - describe('When new email is available', () => { - const setup = () => { - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); - - return { mockStudentUser, mockStudentAccount }; - }; - it('should return true and ignore current users account', async () => { - const { mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - mockStudentAccount.username, - mockStudentUser.id, - mockStudentAccount.id - ); - expect(res).toBe(true); - }); - }); - - describe('When new email already in use by another user', () => { - const setup = () => { - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - const mockAdminUser = userFactory.buildWithId({ - roles: [ - new Role({ - name: RoleName.ADMINISTRATOR, - permissions: [ - Permission.TEACHER_EDIT, - Permission.STUDENT_EDIT, - Permission.STUDENT_LIST, - Permission.TEACHER_LIST, - Permission.TEACHER_CREATE, - Permission.STUDENT_CREATE, - Permission.TEACHER_DELETE, - Permission.STUDENT_DELETE, - ], - }), - ], - }); - const mockAdminAccount = accountDoFactory.build({ userId: mockAdminUser.id }); - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockAdminUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockAdminAccount], 1]); - - return { mockAdminUser, mockStudentUser, mockStudentAccount }; - }; - it('should return false', async () => { - const { mockAdminUser, mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - mockAdminUser.email, - mockStudentUser.id, - mockStudentAccount.id - ); - expect(res).toBe(false); - }); - }); - - describe('When new email already in use by any user and system id is given', () => { - const setup = () => { - const mockTeacherUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const mockTeacherAccount = accountDoFactory.build({ userId: mockTeacherUser.id }); - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockTeacherUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockTeacherAccount], 1]); - - return { mockTeacherAccount, mockStudentUser, mockStudentAccount }; - }; - it('should return false', async () => { - const { mockTeacherAccount, mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - mockTeacherAccount.username, - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); - }); - }); - - describe('When new email already in use by multiple users', () => { - const setup = () => { - const mockTeacherUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - const mockOtherTeacherUser = userFactory.buildWithId({ - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], - }), - ], - }); - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - const mockUsers = [mockTeacherUser, mockStudentUser, mockOtherTeacherUser]; - - userRepo.findByEmail.mockResolvedValueOnce(mockUsers); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); - - return { mockStudentUser, mockStudentAccount }; - }; - it('should return false', async () => { - const { mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - 'multiple@user.email', - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); - }); - }); - - describe('When new email already in use by multiple accounts', () => { - const setup = () => { - const mockTeacherUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - const mockOtherTeacherUser = userFactory.buildWithId({ - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], - }), - ], - }); - - const mockTeacherAccount = accountDoFactory.build({ userId: mockTeacherUser.id }); - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - const mockOtherTeacherAccount = accountDoFactory.build({ - userId: mockOtherTeacherUser.id, - }); - - const mockAccounts = [mockTeacherAccount, mockStudentAccount, mockOtherTeacherAccount]; - userRepo.findByEmail.mockResolvedValueOnce([]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([mockAccounts, mockAccounts.length]); - - return { mockStudentUser, mockStudentAccount }; - }; - it('should return false', async () => { - const { mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - 'multiple@account.username', - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); - }); - }); - - describe('When its another system', () => { - const setup = () => { - const mockExternalUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - const mockOtherExternalUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const externalSystemA = systemFactory.build(); - const externalSystemB = systemFactory.build(); - const mockExternalUserAccount = accountDoFactory.build({ - userId: mockExternalUser.id, - username: 'unique.within@system', - systemId: externalSystemA.id, - }); - const mockOtherExternalUserAccount = accountDoFactory.build({ - userId: mockOtherExternalUser.id, - username: 'unique.within@system', - systemId: externalSystemB.id, - }); - - userRepo.findByEmail.mockResolvedValueOnce([mockExternalUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockExternalUserAccount], 1]); - - return { mockExternalUser, mockExternalUserAccount, mockOtherExternalUserAccount }; - }; - it('should ignore existing username', async () => { - const { mockExternalUser, mockExternalUserAccount, mockOtherExternalUserAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - mockExternalUser.email, - mockExternalUser.id, - mockExternalUserAccount.id, - mockOtherExternalUserAccount.systemId?.toString() - ); - expect(res).toBe(true); - }); - }); - }); - - describe('isUniqueEmailForUser', () => { - describe('When its the email of the given user', () => { - const setup = () => { - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); - accountRepo.findByUserId.mockResolvedValueOnce(mockStudentAccount); - - return { mockStudentUser }; - }; - it('should return true', async () => { - const { mockStudentUser } = setup(); - const res = await accountValidationService.isUniqueEmailForUser(mockStudentUser.email, mockStudentUser.id); - expect(res).toBe(true); - }); - }); - - describe('When its not the given users email', () => { - const setup = () => { - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - const mockAdminUser = userFactory.buildWithId({ - roles: [ - new Role({ - name: RoleName.ADMINISTRATOR, - permissions: [ - Permission.TEACHER_EDIT, - Permission.STUDENT_EDIT, - Permission.STUDENT_LIST, - Permission.TEACHER_LIST, - Permission.TEACHER_CREATE, - Permission.STUDENT_CREATE, - Permission.TEACHER_DELETE, - Permission.STUDENT_DELETE, - ], - }), - ], - }); - const mockAdminAccount = accountDoFactory.build({ userId: mockAdminUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); - accountRepo.findByUserIdOrFail.mockResolvedValueOnce(mockAdminAccount); - - return { mockStudentUser, mockAdminUser }; - }; - it('should return false', async () => { - const { mockStudentUser, mockAdminUser } = setup(); - const res = await accountValidationService.isUniqueEmailForUser(mockStudentUser.email, mockAdminUser.id); - expect(res).toBe(false); - }); - }); - }); - - describe('isUniqueEmailForAccount', () => { - describe('When its the email of the given user', () => { - const setup = () => { - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); - accountRepo.findById.mockResolvedValueOnce(mockStudentAccount); - - return { mockStudentUser, mockStudentAccount }; - }; - it('should return true', async () => { - const { mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmailForAccount( - mockStudentUser.email, - mockStudentAccount.id - ); - expect(res).toBe(true); - }); - }); - describe('When its not the given users email', () => { - const setup = () => { - const mockTeacherUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - const mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - - const mockTeacherAccount = accountDoFactory.build({ userId: mockTeacherUser.id }); - const mockStudentAccount = accountDoFactory.build({ userId: mockStudentUser.id }); - - userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); - accountRepo.findById.mockResolvedValueOnce(mockTeacherAccount); - - return { mockStudentUser, mockTeacherAccount }; - }; - it('should return false', async () => { - const { mockStudentUser, mockTeacherAccount } = setup(); - const res = await accountValidationService.isUniqueEmailForAccount( - mockStudentUser.email, - mockTeacherAccount.id - ); - expect(res).toBe(false); - }); - }); - - describe('When user is missing in account', () => { - const setup = () => { - const oprhanAccount = accountDoFactory.build({ - username: 'orphan@account', - userId: undefined, - systemId: new ObjectId().toHexString(), - }); - - userRepo.findByEmail.mockResolvedValueOnce([]); - accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); - accountRepo.findById.mockResolvedValueOnce(oprhanAccount); - - return { oprhanAccount }; - }; - it('should ignore missing user for given account', async () => { - const { oprhanAccount } = setup(); - const res = await accountValidationService.isUniqueEmailForAccount(oprhanAccount.username, oprhanAccount.id); - expect(res).toBe(true); - }); - }); - }); -}); diff --git a/apps/server/src/modules/account/domain/services/account.validation.service.ts b/apps/server/src/modules/account/domain/services/account.validation.service.ts deleted file mode 100644 index 33ff32a15ad..00000000000 --- a/apps/server/src/modules/account/domain/services/account.validation.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; -import { UserRepo } from '@shared/repo'; -import { AccountRepo } from '../../repo/micro-orm/account.repo'; - -@Injectable() -export class AccountValidationService { - constructor(private accountRepo: AccountRepo, private userRepo: UserRepo) {} - - async isUniqueEmail(email: string, userId?: EntityId, accountId?: EntityId, systemId?: EntityId): Promise { - const foundUsers = await this.userRepo.findByEmail(email); - const [accounts] = await this.accountRepo.searchByUsernameExactMatch(email); - const filteredAccounts = accounts.filter((foundAccount) => foundAccount.systemId === systemId); - - const multipleUsers = foundUsers.length > 1; - const multipleAccounts = filteredAccounts.length > 1; - // paranoid 'toString': legacy code may call userId or accountId as ObjectID - const oneUserWithoutGivenId = foundUsers.length === 1 && foundUsers[0].id.toString() !== userId?.toString(); - const oneAccountWithoutGivenId = - filteredAccounts.length === 1 && filteredAccounts[0].id.toString() !== accountId?.toString(); - - const isUnique = !(multipleUsers || multipleAccounts || oneUserWithoutGivenId || oneAccountWithoutGivenId); - - return isUnique; - } - - async isUniqueEmailForUser(email: string, userId: EntityId): Promise { - const account = await this.accountRepo.findByUserId(userId); - return this.isUniqueEmail(email, userId, account?.id, account?.systemId?.toString()); - } - - async isUniqueEmailForAccount(email: string, accountId: EntityId): Promise { - const account = await this.accountRepo.findById(accountId); - return this.isUniqueEmail(email, account.userId?.toString(), account.id, account.systemId?.toString()); - } -} diff --git a/apps/server/src/modules/account/repo/micro-orm/account.repo.integration.spec.ts b/apps/server/src/modules/account/repo/micro-orm/account.repo.integration.spec.ts index c6fc186c726..4fbdd62fe32 100644 --- a/apps/server/src/modules/account/repo/micro-orm/account.repo.integration.spec.ts +++ b/apps/server/src/modules/account/repo/micro-orm/account.repo.integration.spec.ts @@ -4,11 +4,11 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { User } from '@shared/domain/entity'; import { cleanupCollections, userFactory } from '@shared/testing'; -import { AccountRepo } from './account.repo'; import { AccountEntity } from '../../domain/entity/account.entity'; -import { AccountDoToEntityMapper } from './mapper/account-do-to-entity.mapper'; -import { AccountEntityToDoMapper } from './mapper'; import { accountDoFactory, accountFactory } from '../../testing'; +import { AccountRepo } from './account.repo'; +import { AccountEntityToDoMapper } from './mapper'; +import { AccountDoToEntityMapper } from './mapper/account-do-to-entity.mapper'; describe('account repo', () => { let module: TestingModule; @@ -115,6 +115,35 @@ describe('account repo', () => { }); }); + describe('findByUsername', () => { + describe('When username is given', () => { + const setup = async () => { + const accountToFind = accountFactory.build(); + + await em.persistAndFlush(accountToFind); + em.clear(); + + return accountToFind; + }; + + it('should find user by username', async () => { + const accountToFind = await setup(); + + const account = await repo.findByUsername(accountToFind.username); + + expect(account?.username).toEqual(accountToFind.username); + }); + }); + + describe('When username is not given', () => { + it('should return null', async () => { + const account = await repo.findByUsername(''); + + expect(account).toBeNull(); + }); + }); + }); + describe('findByUsernameAndSystemId', () => { describe('When username and systemId are given', () => { const setup = async () => { diff --git a/apps/server/src/modules/account/repo/micro-orm/account.repo.ts b/apps/server/src/modules/account/repo/micro-orm/account.repo.ts index d6566afa1c0..38c48f558f6 100644 --- a/apps/server/src/modules/account/repo/micro-orm/account.repo.ts +++ b/apps/server/src/modules/account/repo/micro-orm/account.repo.ts @@ -3,10 +3,10 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; -import { AccountEntity } from '../../domain/entity/account.entity'; -import { AccountDoToEntityMapper } from './mapper/account-do-to-entity.mapper'; import { Account } from '../../domain/account'; +import { AccountEntity } from '../../domain/entity/account.entity'; import { AccountEntityToDoMapper } from './mapper'; +import { AccountDoToEntityMapper } from './mapper/account-do-to-entity.mapper'; import { AccountScope } from './scope/account-scope'; @Injectable() @@ -66,6 +66,16 @@ export class AccountRepo { return AccountEntityToDoMapper.mapToDo(entity); } + public async findByUsername(username: string): Promise { + const entity = await this.em.findOne(AccountEntity, { username }); + + if (!entity) { + return null; + } + + return AccountEntityToDoMapper.mapToDo(entity); + } + public async findByUsernameAndSystemId(username: string, systemId: EntityId | ObjectId): Promise { const entity = await this.em.findOne(AccountEntity, { username, systemId: new ObjectId(systemId) }); diff --git a/apps/server/src/modules/account/testing/account.factory.ts b/apps/server/src/modules/account/testing/account.factory.ts index 8c8177f1a9e..0cfa746e4cb 100644 --- a/apps/server/src/modules/account/testing/account.factory.ts +++ b/apps/server/src/modules/account/testing/account.factory.ts @@ -3,9 +3,9 @@ import { User } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DeepPartial } from 'fishery'; -import { AccountEntity, IdmAccountProperties } from '@src/modules/account/domain/entity/account.entity'; import { BaseFactory } from '@shared/testing/factory/base.factory'; +import { AccountEntity, IdmAccountProperties } from '@src/modules/account/domain/entity/account.entity'; +import { DeepPartial } from 'fishery'; export const defaultTestPassword = 'DummyPasswd!1'; export const defaultTestPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; @@ -58,7 +58,7 @@ class AccountFactory extends BaseFactory { // !!! important username should not be contain a space !!! export const accountFactory = AccountFactory.define(AccountEntity, ({ sequence }) => { return { - username: `account${sequence}`, + username: `account#${sequence}@example.tld`, password: defaultTestPasswordHash, userId: new ObjectId(), }; diff --git a/src/services/user/firstLogin.js b/src/services/user/firstLogin.js index 0d3358c2c6d..b441fc3e1e7 100644 --- a/src/services/user/firstLogin.js +++ b/src/services/user/firstLogin.js @@ -77,14 +77,6 @@ const firstLogin = async (data, params, app) => { userUpdate.birthday = parseDate(data.studentBirthdate); } - // email - if (data['student-email']) { - if (!constants.expressions.email.test(data['student-email'])) { - throw new Error('Bitte eine valide E-Mail-Adresse eingeben.'); - } - userUpdate.email = data['student-email']; - } - // TODO: consent also part of user now, why not patch by this request. Is the third parameter really needed? userPromise = app.service('users').patch(user._id, userUpdate, { account: params.account }); diff --git a/src/services/user/hooks/userService.js b/src/services/user/hooks/userService.js index b2b33a60e7f..7a76abc3f57 100644 --- a/src/services/user/hooks/userService.js +++ b/src/services/user/hooks/userService.js @@ -1,632 +1,630 @@ -const { authenticate } = require('@feathersjs/authentication'); -const { keep } = require('feathers-hooks-common'); - -const { Forbidden, NotFound, BadRequest, GeneralError } = require('../../../errors'); -const logger = require('../../../logger'); -const { ObjectId } = require('../../../helper/compare'); -const { hasRoleNoHook, hasPermissionNoHook, hasPermission } = require('../../../hooks'); - -const { getAge } = require('../../../utils'); - -const constants = require('../../../utils/constants'); -const { CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS, SC_DOMAIN } = require('../../../../config/globals'); - -/** - * - * @param {object} hook - The hook of the server-request, containing req.params.query.roles as role-filter - * @returns {Promise } - */ -const mapRoleFilterQuery = (hook) => { - if (hook.params.query.roles) { - const rolesFilter = hook.params.query.roles; - hook.params.query.roles = {}; - hook.params.query.roles.$in = rolesFilter; - } - - return Promise.resolve(hook); -}; -const getProtectedRoles = (hook) => - hook.app.service('/roles').find({ - // load protected roles - query: { - // TODO: cache these - name: ['teacher', 'admin'], - }, - }); - -const checkUnique = (hook) => { - const userService = hook.service; - const { email } = hook.data; - if (email === undefined) { - return Promise.reject(new BadRequest('Fehler beim Auslesen der E-Mail-Adresse bei der Nutzererstellung.')); - } - return userService.find({ query: { email: email.toLowerCase() } }).then((result) => { - const { length } = result.data; - if (length === undefined || length >= 2) { - return Promise.reject(new BadRequest('Fehler beim Prüfen der Datenbankinformationen.')); - } - if (length === 0) { - return Promise.resolve(hook); - } - - const user = typeof result.data[0] === 'object' ? result.data[0] : {}; - const input = typeof hook.data === 'object' ? hook.data : {}; - const isLoggedIn = (hook.params || {}).account && hook.params.account.userId; - // eslint-disable-next-line no-underscore-dangle - const { asTask } = hook.params._additional || {}; - - if (isLoggedIn || asTask === undefined || asTask === 'student') { - return Promise.reject(new BadRequest(`Die E-Mail Adresse ist bereits in Verwendung!`)); - } - return Promise.resolve(hook); - }); -}; - -const checkUniqueEmail = async (hook) => { - const { email } = hook.data; - if (!email) { - // there is no email address given. Nothing to check... - return Promise.resolve(hook); - } - - // get userId of user entry to edit - const editUserId = hook.id ? hook.id.toString() : undefined; - const unique = await hook.app.service('nest-account-validation-service').isUniqueEmailForUser(email, editUserId); - - if (unique) { - return hook; - } - throw new BadRequest(`Die E-Mail Adresse ist bereits in Verwendung!`); -}; - -const checkUniqueAccount = (hook) => { - const { email } = hook.data; - return hook.app - .service('nest-account-service') - .searchByUsernameExactMatch(email.toLowerCase()) - .then(([result]) => { - if (result.length > 0) { - throw new BadRequest(`Ein Account mit dieser E-Mail Adresse ${email} existiert bereits!`); - } - return hook; - }); -}; - -const updateAccountUsername = async (context) => { - let { - params: { account }, - } = context; - const { - data: { email }, - app, - } = context; - - if (!email) { - return context; - } - - if (!context.id) { - throw new BadRequest('Id is required for email changes'); - } - - if (!account || !ObjectId.equal(context.id, account.userId)) { - account = await app.service('nest-account-service').findByUserId(context.id); - - if (!account) return context; - } - - if (email && account.systemId) { - delete context.data.email; - return context; - } - - await app - .service('nest-account-service') - .updateUsername(account.id ? account.id : account._id.toString(), email) - .catch((err) => { - throw new BadRequest('Can not update account username.', err); - }); - return context; -}; - -const removeStudentFromClasses = async (hook) => { - // todo: move this functionality into classes, using events. - // todo: what about teachers? - const classesService = hook.app.service('/classes'); - const userIds = hook.id || (hook.result || []).map((u) => u._id); - if (userIds === undefined) { - throw new BadRequest( - 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.' - ); - } - - try { - const usersClasses = await classesService.find({ query: { userIds: { $in: userIds } } }); - await Promise.all( - usersClasses.data.map((klass) => classesService.patch(klass._id, { $pull: { userIds: { $in: userIds } } })) - ); - } catch (err) { - throw new Forbidden( - 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.', - err - ); - } - - return hook; -}; - -const removeStudentFromCourses = async (hook) => { - // todo: move this functionality into courses, using events. - // todo: what about teachers? - const coursesService = hook.app.service('/courses'); - const userIds = hook.id || (hook.result || []).map((u) => u._id); - if (userIds === undefined) { - throw new BadRequest( - 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.' - ); - } - - try { - const usersCourses = await coursesService.find({ query: { userIds: { $in: userIds } } }); - await Promise.all( - usersCourses.data.map((course) => - hook.app.service('courseModel').patch(course._id, { $pull: { userIds: { $in: userIds } } }) - ) - ); - } catch (err) { - throw new Forbidden( - 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.', - err - ); - } -}; - -const sanitizeData = (hook) => { - if ('email' in hook.data) { - if (!constants.expressions.email.test(hook.data.email)) { - return Promise.reject(new BadRequest('Bitte gib eine valide E-Mail Adresse an!')); - } - } - const idRegExp = RegExp('^[0-9a-fA-F]{24}$'); - if ('schoolId' in hook.data) { - if (!idRegExp.test(hook.data.schoolId)) { - return Promise.reject(new BadRequest('invalid Id')); - } - } - if ('classId' in hook.data) { - if (!idRegExp.test(hook.data.classId)) { - return Promise.reject(new BadRequest('invalid Id')); - } - } - return Promise.resolve(hook); -}; - -const checkJwt = () => - function checkJwtfnc(hook) { - if (((hook.params || {}).headers || {}).authorization !== undefined) { - return authenticate('jwt').call(this, hook); - } - return Promise.resolve(hook); - }; - -const pinIsVerified = (hook) => { - if ((hook.params || {}).account && hook.params.account.userId) { - return hasPermission(['STUDENT_CREATE', 'TEACHER_CREATE', 'ADMIN_CREATE']).call(this, hook); - } - // eslint-disable-next-line no-underscore-dangle - const email = (hook.params._additional || {}).parentEmail || hook.data.email; - return hook.app - .service('/registrationPins') - .find({ query: { email, verified: true } }) - .then((pins) => { - if (pins.data.length === 1 && pins.data[0].pin) { - const age = getAge(hook.data.birthday); - - if (!((hook.data.roles || []).includes('student') && age < CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS)) { - hook.app.service('/registrationPins').remove(pins.data[0]._id); - } - - return Promise.resolve(hook); - } - return Promise.reject(new BadRequest('Der Pin wurde noch nicht bei der Registrierung eingetragen.')); - }); -}; - -const protectImmutableAttributes = async (context) => { - const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); - if (userIsSuperhero) return context; - - delete context.data.roles; - delete (context.data.$push || {}).roles; - delete (context.data.$pull || {}).roles; - delete (context.data.$pop || {}).roles; - delete (context.data.$addToSet || {}).roles; - delete (context.data.$pullAll || {}).roles; - delete (context.data.$set || {}).roles; - - delete context.data.schoolId; - delete (context.data.$set || {}).schoolId; - - return context; -}; - -const securePatching = async (context) => { - const targetUser = await context.app.service('users').get(context.id, { query: { $populate: 'roles' } }); - const actingUser = await context.app - .service('users') - .get(context.params.account.userId, { query: { $populate: 'roles' } }); - const isSuperHero = actingUser.roles.find((r) => r.name === 'superhero'); - const isAdmin = actingUser.roles.find((r) => r.name === 'administrator'); - const isTeacher = actingUser.roles.find((r) => r.name === 'teacher'); - const targetIsStudent = targetUser.roles.find((r) => r.name === 'student'); - - if (isSuperHero) { - return context; - } - - if (!ObjectId.equal(targetUser.schoolId, actingUser.schoolId)) { - return Promise.reject(new NotFound(`no record found for id '${context.id.toString()}'`)); - } - - if (!ObjectId.equal(context.id, context.params.account.userId)) { - if (!(isAdmin || (isTeacher && targetIsStudent))) { - return Promise.reject(new BadRequest('You have not the permissions to change other users')); - } - } - return Promise.resolve(context); -}; - -const formatLastName = (name, isOutdated) => `${name}${isOutdated ? ' ~~' : ''}`; - -/** - * - * @param user {object} - the user the display name has to be generated - * @param app {object} - the global feathers-app - * @returns {string} - a display name of the given user - */ -const getDisplayName = (user, protectedRoles) => { - const protectedRoleIds = (protectedRoles.data || []).map((role) => role._id); - const isProtectedUser = protectedRoleIds.find((role) => (user.roles || []).includes(role)); - - const isOutdated = !!user.outdatedSince; - - user.age = getAge(user.birthday); - - if (isProtectedUser) { - return user.lastName ? formatLastName(user.lastName, isOutdated) : user._id; - } - return user.lastName ? `${user.firstName} ${formatLastName(user.lastName, isOutdated)}` : user._id; -}; - -/** - * - * @param hook {object} - the hook of the server-request - * @returns {object} - the hook with the decorated user - */ -const decorateUser = async (hook) => { - const protectedRoles = await getProtectedRoles(hook); - const displayName = getDisplayName(hook.result, protectedRoles); - hook.result = hook.result.constructor.name === 'model' ? hook.result.toObject() : hook.result; - hook.result.displayName = displayName; - return hook; -}; - -/** - * - * @param user {object} - a user - * @returns {object} - a user with avatar info - */ -const setAvatarData = (user) => { - if (user.firstName && user.lastName) { - user.avatarInitials = user.firstName.charAt(0) + user.lastName.charAt(0); - } else { - user.avatarInitials = '?'; - } - // css readable value like "#ff0000" needed - const colors = ['#4a4e4d', '#0e9aa7', '#3da4ab', '#f6cd61', '#fe8a71']; - if (user.customAvatarBackgroundColor) { - user.avatarBackgroundColor = user.customAvatarBackgroundColor; - } else { - // choose colors based on initials - const index = (user.avatarInitials.charCodeAt(0) + user.avatarInitials.charCodeAt(1)) % colors.length; - user.avatarBackgroundColor = colors[index]; - } - return user; -}; - -/** - * - * @param hook {object} - the hook of the server-request - * @returns {object} - the hook with the decorated user avatar - */ -const decorateAvatar = (hook) => { - if (hook.result.total) { - hook.result = hook.result.constructor.name === 'model' ? hook.result.toObject() : hook.result; - (hook.result.data || []).forEach((user) => setAvatarData(user)); - } else { - // run and find with only one user - hook.result = setAvatarData(hook.result); - } - - return Promise.resolve(hook); -}; - -/** - * - * @param hook {object} - the hook of the server-request - * @returns {object} - the hook with the decorated users - */ -const decorateUsers = async (hook) => { - hook.result = hook.result.constructor.name === 'model' ? hook.result.toObject() : hook.result; - const protectedRoles = await getProtectedRoles(hook); - const users = (hook.result.data || []).map((user) => { - user.displayName = getDisplayName(user, protectedRoles); - return user; - }); - hook.result.data = users; - return hook; -}; - -const handleClassId = (hook) => { - if (!('classId' in hook.data)) { - return Promise.resolve(hook); - } - return hook.app - .service('/classes') - .patch(hook.data.classId, { - $push: { userIds: hook.result._id }, - }) - .then((res) => Promise.resolve(hook)); -}; - -const pushRemoveEvent = (hook) => { - hook.app.emit('users:after:remove', hook); - return hook; -}; - -const enforceRoleHierarchyOnDeleteSingle = async (context) => { - try { - const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); - if (userIsSuperhero) return context; - - const [targetIsStudent, targetIsTeacher, targetIsAdmin] = await Promise.all([ - hasRoleNoHook(context, context.id, 'student'), - hasRoleNoHook(context, context.id, 'teacher'), - hasRoleNoHook(context, context.id, 'administrator'), - ]); - let permissionChecks = [true]; - if (targetIsStudent) { - permissionChecks.push(hasPermissionNoHook(context, context.params.account.userId, 'STUDENT_DELETE')); - } - if (targetIsTeacher) { - permissionChecks.push(hasPermissionNoHook(context, context.params.account.userId, 'TEACHER_DELETE')); - } - if (targetIsAdmin) { - permissionChecks.push(hasRoleNoHook(context, context.params.account.userId, 'superhero')); - } - permissionChecks = await Promise.all(permissionChecks); - - if (!permissionChecks.reduce((accumulator, val) => accumulator && val)) { - throw new Forbidden('you dont have permission to delete this user!'); - } - - return context; - } catch (error) { - logger.error(error); - throw new Forbidden('you dont have permission to delete this user!'); - } -}; - -const enforceRoleHierarchyOnDeleteBulk = async (context) => { - const user = await context.app.service('users').get(context.params.account.userId); - const canDeleteStudent = user.permissions.includes('STUDENT_DELETE'); - const canDeleteTeacher = user.permissions.includes('TEACHER_DELETE'); - const rolePromises = []; - if (canDeleteStudent) { - rolePromises.push( - context.app - .service('roles') - .find({ query: { name: 'student' } }) - .then((r) => r.data[0]._id) - ); - } - if (canDeleteTeacher) { - rolePromises.push( - context.app - .service('roles') - .find({ query: { name: 'teacher' } }) - .then((r) => r.data[0]._id) - ); - } - const allowedRoles = await Promise.all(rolePromises); - - // there may not be any role in user.roles that is not in rolesToDelete - const roleQuery = { $nor: [{ roles: { $elemMatch: { $nin: allowedRoles } } }] }; - context.params.query = { $and: [context.params.query, roleQuery] }; - return context; -}; - -const enforceRoleHierarchyOnDelete = async (context) => { - if (context.id) return enforceRoleHierarchyOnDeleteSingle(context); - return enforceRoleHierarchyOnDeleteBulk(context); -}; - -/** - * Check that the authenticated user posseses the rights to create a user with the given roles. - * This is only checked for external requests. - * @param {*} context - */ -const enforceRoleHierarchyOnCreate = async (context) => { - const user = await context.app.service('users').get(context.params.account.userId, { query: { $populate: 'roles' } }); - - // superhero may create users with every role - if (user.roles.filter((u) => u.name === 'superhero').length > 0) { - return Promise.resolve(context); - } - - // created user has no role - if (!context.data || !context.data.roles) { - return Promise.resolve(context); - } - await Promise.all( - context.data.roles.map(async (roleId) => { - // Roles are given by ID or by name. - // For IDs we load the name from the DB. - // If it is not an ID we assume, it is a name. Invalid names are rejected in the switch anyways. - let roleName = ''; - if (!ObjectId.isValid(roleId)) { - roleName = roleId; - } else { - try { - const role = await context.app.service('roles').get(roleId); - roleName = role.name; - } catch (exception) { - return Promise.reject(new BadRequest('No such role exists')); - } - } - switch (roleName) { - case 'teacher': - if (!user.permissions.find((permission) => permission === 'TEACHER_CREATE')) { - return Promise.reject(new BadRequest('Your are not allowed to create a user with the given role')); - } - break; - case 'student': - if (!user.permissions.find((permission) => permission === 'STUDENT_CREATE')) { - return Promise.reject(new BadRequest('Your are not allowed to create a user with the given role')); - } - break; - case 'parent': - break; - default: - return Promise.reject(new BadRequest('Your are not allowed to create a user with the given role')); - } - return Promise.resolve(context); - }) - ); - - return Promise.resolve(context); -}; - -const generateRegistrationLink = async (context) => { - const { data, app } = context; - if (data.generateRegistrationLink === true) { - delete data.generateRegistrationLink; - if (!data.roles || data.roles.length > 1) { - throw new BadRequest('Roles must be exactly of length one if generateRegistrationLink=true is set.'); - } - const { hash } = await app - .service('/registrationlink') - // set account in params to context.parmas.account to reference the current user - .create({ - role: data.roles[0], - save: true, - patchUser: true, - host: SC_DOMAIN, - schoolId: data.schoolId, - toHash: data.email, - }) - .catch((err) => { - throw new GeneralError(`Can not create registrationlink. ${err}`); - }); - context.data.importHash = hash; - } -}; - -const sendRegistrationLink = async (context) => { - const { result, data, app } = context; - if (data.sendRegistration === true) { - delete data.sendRegistration; - await app.service('/users/mail/registrationLink').create({ - users: [result], - }); - } - return context; -}; - -const filterResult = async (context) => { - const userCallingHimself = context.id && ObjectId.equal(context.id, context.params.account.userId); - const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); - if (userCallingHimself || userIsSuperhero) { - return context; - } - - const allowedAttributes = [ - '_id', - 'roles', - 'schoolId', - 'firstName', - 'middleName', - 'lastName', - 'namePrefix', - 'nameSuffix', - 'discoverable', - 'fullName', - 'displayName', - 'avatarInitials', - 'avatarBackgroundColor', - 'outdatedSince', - ]; - return keep(...allowedAttributes)(context); -}; - -let roleCache = null; -const includeOnlySchoolRoles = async (context) => { - if (context.params && context.params.query) { - const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); - if (userIsSuperhero) { - return context; - } - - // todo: remove with static role service (SC-3731) - if (!Array.isArray(roleCache)) { - roleCache = ( - await context.app.service('roles').find({ - query: { - name: { $in: ['administrator', 'teacher', 'student'] }, - }, - paginate: false, - }) - ).map((r) => r._id); - } - const allowedRoles = roleCache; - - if (context.params.query.roles && context.params.query.roles.$in) { - // when querying for specific roles, filter them - context.params.query.roles.$in = context.params.query.roles.$in.filter((r) => - allowedRoles.some((a) => ObjectId.equal(r, a)) - ); - } else { - // otherwise, overwrite them with whitelist - context.params.query.roles = { - $in: allowedRoles, - }; - } - } - return context; -}; - -module.exports = { - mapRoleFilterQuery, - checkUnique, - checkUniqueEmail, - checkJwt, - checkUniqueAccount, - updateAccountUsername, - removeStudentFromClasses, - removeStudentFromCourses, - sanitizeData, - pinIsVerified, - protectImmutableAttributes, - securePatching, - decorateUser, - decorateAvatar, - decorateUsers, - handleClassId, - pushRemoveEvent, - enforceRoleHierarchyOnDelete, - enforceRoleHierarchyOnCreate, - filterResult, - generateRegistrationLink, - sendRegistrationLink, - includeOnlySchoolRoles, -}; +const { authenticate } = require('@feathersjs/authentication'); +const { keep } = require('feathers-hooks-common'); + +const { Forbidden, NotFound, BadRequest, GeneralError } = require('../../../errors'); +const logger = require('../../../logger'); +const { ObjectId } = require('../../../helper/compare'); +const { hasRoleNoHook, hasPermissionNoHook, hasPermission } = require('../../../hooks'); + +const { getAge } = require('../../../utils'); + +const constants = require('../../../utils/constants'); +const { CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS, SC_DOMAIN } = require('../../../../config/globals'); + +/** + * + * @param {object} hook - The hook of the server-request, containing req.params.query.roles as role-filter + * @returns {Promise } + */ +const mapRoleFilterQuery = (hook) => { + if (hook.params.query.roles) { + const rolesFilter = hook.params.query.roles; + hook.params.query.roles = {}; + hook.params.query.roles.$in = rolesFilter; + } + + return Promise.resolve(hook); +}; +const getProtectedRoles = (hook) => + hook.app.service('/roles').find({ + // load protected roles + query: { + // TODO: cache these + name: ['teacher', 'admin'], + }, + }); + +const checkUnique = (hook) => { + const userService = hook.service; + const { email } = hook.data; + if (email === undefined) { + return Promise.reject(new BadRequest('Fehler beim Auslesen der E-Mail-Adresse bei der Nutzererstellung.')); + } + return userService.find({ query: { email: email.toLowerCase() } }).then((result) => { + const { length } = result.data; + if (length === undefined || length >= 2) { + return Promise.reject(new BadRequest('Fehler beim Prüfen der Datenbankinformationen.')); + } + if (length === 0) { + return Promise.resolve(hook); + } + + const user = typeof result.data[0] === 'object' ? result.data[0] : {}; + const input = typeof hook.data === 'object' ? hook.data : {}; + const isLoggedIn = (hook.params || {}).account && hook.params.account.userId; + // eslint-disable-next-line no-underscore-dangle + const { asTask } = hook.params._additional || {}; + + if (isLoggedIn || asTask === undefined || asTask === 'student') { + return Promise.reject(new BadRequest(`Die E-Mail Adresse ist bereits in Verwendung!`)); + } + return Promise.resolve(hook); + }); +}; + +const checkUniqueEmail = async (hook) => { + const { email } = hook.data; + if (!email) { + // there is no email address given. Nothing to check... + return Promise.resolve(hook); + } + + const isUnique = await hook.app.service('nest-account-service').isUniqueEmail(email); + + if (isUnique) { + return hook; + } + throw new BadRequest(`Die E-Mail Adresse ist bereits in Verwendung!`); +}; + +const checkUniqueAccount = (hook) => { + const { email } = hook.data; + return hook.app + .service('nest-account-service') + .searchByUsernameExactMatch(email.toLowerCase()) + .then(([result]) => { + if (result.length > 0) { + throw new BadRequest(`Ein Account mit dieser E-Mail Adresse ${email} existiert bereits!`); + } + return hook; + }); +}; + +const updateAccountUsername = async (context) => { + let { + params: { account }, + } = context; + const { + data: { email }, + app, + } = context; + + if (!email) { + return context; + } + + if (!context.id) { + throw new BadRequest('Id is required for email changes'); + } + + if (!account || !ObjectId.equal(context.id, account.userId)) { + account = await app.service('nest-account-service').findByUserId(context.id); + + if (!account) return context; + } + + if (email && account.systemId) { + delete context.data.email; + return context; + } + + await app + .service('nest-account-service') + .updateUsername(account.id ? account.id : account._id.toString(), email) + .catch((err) => { + throw new BadRequest('Can not update account username.', err); + }); + return context; +}; + +const removeStudentFromClasses = async (hook) => { + // todo: move this functionality into classes, using events. + // todo: what about teachers? + const classesService = hook.app.service('/classes'); + const userIds = hook.id || (hook.result || []).map((u) => u._id); + if (userIds === undefined) { + throw new BadRequest( + 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.' + ); + } + + try { + const usersClasses = await classesService.find({ query: { userIds: { $in: userIds } } }); + await Promise.all( + usersClasses.data.map((klass) => classesService.patch(klass._id, { $pull: { userIds: { $in: userIds } } })) + ); + } catch (err) { + throw new Forbidden( + 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.', + err + ); + } + + return hook; +}; + +const removeStudentFromCourses = async (hook) => { + // todo: move this functionality into courses, using events. + // todo: what about teachers? + const coursesService = hook.app.service('/courses'); + const userIds = hook.id || (hook.result || []).map((u) => u._id); + if (userIds === undefined) { + throw new BadRequest( + 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.' + ); + } + + try { + const usersCourses = await coursesService.find({ query: { userIds: { $in: userIds } } }); + await Promise.all( + usersCourses.data.map((course) => + hook.app.service('courseModel').patch(course._id, { $pull: { userIds: { $in: userIds } } }) + ) + ); + } catch (err) { + throw new Forbidden( + 'Der Nutzer wurde gelöscht, konnte aber eventuell nicht aus allen Klassen/Kursen entfernt werden.', + err + ); + } +}; + +const sanitizeData = (hook) => { + if ('email' in hook.data) { + if (!constants.expressions.email.test(hook.data.email)) { + return Promise.reject(new BadRequest('Bitte gib eine valide E-Mail Adresse an!')); + } + } + const idRegExp = RegExp('^[0-9a-fA-F]{24}$'); + if ('schoolId' in hook.data) { + if (!idRegExp.test(hook.data.schoolId)) { + return Promise.reject(new BadRequest('invalid Id')); + } + } + if ('classId' in hook.data) { + if (!idRegExp.test(hook.data.classId)) { + return Promise.reject(new BadRequest('invalid Id')); + } + } + return Promise.resolve(hook); +}; + +const checkJwt = () => + function checkJwtfnc(hook) { + if (((hook.params || {}).headers || {}).authorization !== undefined) { + return authenticate('jwt').call(this, hook); + } + return Promise.resolve(hook); + }; + +const pinIsVerified = (hook) => { + if ((hook.params || {}).account && hook.params.account.userId) { + return hasPermission(['STUDENT_CREATE', 'TEACHER_CREATE', 'ADMIN_CREATE']).call(this, hook); + } + // eslint-disable-next-line no-underscore-dangle + const email = (hook.params._additional || {}).parentEmail || hook.data.email; + return hook.app + .service('/registrationPins') + .find({ query: { email, verified: true } }) + .then((pins) => { + if (pins.data.length === 1 && pins.data[0].pin) { + const age = getAge(hook.data.birthday); + + if (!((hook.data.roles || []).includes('student') && age < CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS)) { + hook.app.service('/registrationPins').remove(pins.data[0]._id); + } + + return Promise.resolve(hook); + } + return Promise.reject(new BadRequest('Der Pin wurde noch nicht bei der Registrierung eingetragen.')); + }); +}; + +const protectImmutableAttributes = async (context) => { + const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); + if (userIsSuperhero) return context; + + delete context.data.roles; + delete (context.data.$push || {}).roles; + delete (context.data.$pull || {}).roles; + delete (context.data.$pop || {}).roles; + delete (context.data.$addToSet || {}).roles; + delete (context.data.$pullAll || {}).roles; + delete (context.data.$set || {}).roles; + + delete context.data.schoolId; + delete (context.data.$set || {}).schoolId; + + return context; +}; + +const securePatching = async (context) => { + const targetUser = await context.app.service('users').get(context.id, { query: { $populate: 'roles' } }); + const actingUser = await context.app + .service('users') + .get(context.params.account.userId, { query: { $populate: 'roles' } }); + const isSuperHero = actingUser.roles.find((r) => r.name === 'superhero'); + const isAdmin = actingUser.roles.find((r) => r.name === 'administrator'); + const isTeacher = actingUser.roles.find((r) => r.name === 'teacher'); + const targetIsStudent = targetUser.roles.find((r) => r.name === 'student'); + + if (isSuperHero) { + return context; + } + + if (!ObjectId.equal(targetUser.schoolId, actingUser.schoolId)) { + return Promise.reject(new NotFound(`no record found for id '${context.id.toString()}'`)); + } + + if (!ObjectId.equal(context.id, context.params.account.userId)) { + if (!(isAdmin || (isTeacher && targetIsStudent))) { + return Promise.reject(new BadRequest('You have not the permissions to change other users')); + } + } + return Promise.resolve(context); +}; + +const formatLastName = (name, isOutdated) => `${name}${isOutdated ? ' ~~' : ''}`; + +/** + * + * @param user {object} - the user the display name has to be generated + * @param app {object} - the global feathers-app + * @returns {string} - a display name of the given user + */ +const getDisplayName = (user, protectedRoles) => { + const protectedRoleIds = (protectedRoles.data || []).map((role) => role._id); + const isProtectedUser = protectedRoleIds.find((role) => (user.roles || []).includes(role)); + + const isOutdated = !!user.outdatedSince; + + user.age = getAge(user.birthday); + + if (isProtectedUser) { + return user.lastName ? formatLastName(user.lastName, isOutdated) : user._id; + } + return user.lastName ? `${user.firstName} ${formatLastName(user.lastName, isOutdated)}` : user._id; +}; + +/** + * + * @param hook {object} - the hook of the server-request + * @returns {object} - the hook with the decorated user + */ +const decorateUser = async (hook) => { + const protectedRoles = await getProtectedRoles(hook); + const displayName = getDisplayName(hook.result, protectedRoles); + hook.result = hook.result.constructor.name === 'model' ? hook.result.toObject() : hook.result; + hook.result.displayName = displayName; + return hook; +}; + +/** + * + * @param user {object} - a user + * @returns {object} - a user with avatar info + */ +const setAvatarData = (user) => { + if (user.firstName && user.lastName) { + user.avatarInitials = user.firstName.charAt(0) + user.lastName.charAt(0); + } else { + user.avatarInitials = '?'; + } + // css readable value like "#ff0000" needed + const colors = ['#4a4e4d', '#0e9aa7', '#3da4ab', '#f6cd61', '#fe8a71']; + if (user.customAvatarBackgroundColor) { + user.avatarBackgroundColor = user.customAvatarBackgroundColor; + } else { + // choose colors based on initials + const index = (user.avatarInitials.charCodeAt(0) + user.avatarInitials.charCodeAt(1)) % colors.length; + user.avatarBackgroundColor = colors[index]; + } + return user; +}; + +/** + * + * @param hook {object} - the hook of the server-request + * @returns {object} - the hook with the decorated user avatar + */ +const decorateAvatar = (hook) => { + if (hook.result.total) { + hook.result = hook.result.constructor.name === 'model' ? hook.result.toObject() : hook.result; + (hook.result.data || []).forEach((user) => setAvatarData(user)); + } else { + // run and find with only one user + hook.result = setAvatarData(hook.result); + } + + return Promise.resolve(hook); +}; + +/** + * + * @param hook {object} - the hook of the server-request + * @returns {object} - the hook with the decorated users + */ +const decorateUsers = async (hook) => { + hook.result = hook.result.constructor.name === 'model' ? hook.result.toObject() : hook.result; + const protectedRoles = await getProtectedRoles(hook); + const users = (hook.result.data || []).map((user) => { + user.displayName = getDisplayName(user, protectedRoles); + return user; + }); + hook.result.data = users; + return hook; +}; + +const handleClassId = (hook) => { + if (!('classId' in hook.data)) { + return Promise.resolve(hook); + } + return hook.app + .service('/classes') + .patch(hook.data.classId, { + $push: { userIds: hook.result._id }, + }) + .then((res) => Promise.resolve(hook)); +}; + +const pushRemoveEvent = (hook) => { + hook.app.emit('users:after:remove', hook); + return hook; +}; + +const enforceRoleHierarchyOnDeleteSingle = async (context) => { + try { + const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); + if (userIsSuperhero) return context; + + const [targetIsStudent, targetIsTeacher, targetIsAdmin] = await Promise.all([ + hasRoleNoHook(context, context.id, 'student'), + hasRoleNoHook(context, context.id, 'teacher'), + hasRoleNoHook(context, context.id, 'administrator'), + ]); + let permissionChecks = [true]; + if (targetIsStudent) { + permissionChecks.push(hasPermissionNoHook(context, context.params.account.userId, 'STUDENT_DELETE')); + } + if (targetIsTeacher) { + permissionChecks.push(hasPermissionNoHook(context, context.params.account.userId, 'TEACHER_DELETE')); + } + if (targetIsAdmin) { + permissionChecks.push(hasRoleNoHook(context, context.params.account.userId, 'superhero')); + } + permissionChecks = await Promise.all(permissionChecks); + + if (!permissionChecks.reduce((accumulator, val) => accumulator && val)) { + throw new Forbidden('you dont have permission to delete this user!'); + } + + return context; + } catch (error) { + logger.error(error); + throw new Forbidden('you dont have permission to delete this user!'); + } +}; + +const enforceRoleHierarchyOnDeleteBulk = async (context) => { + const user = await context.app.service('users').get(context.params.account.userId); + const canDeleteStudent = user.permissions.includes('STUDENT_DELETE'); + const canDeleteTeacher = user.permissions.includes('TEACHER_DELETE'); + const rolePromises = []; + if (canDeleteStudent) { + rolePromises.push( + context.app + .service('roles') + .find({ query: { name: 'student' } }) + .then((r) => r.data[0]._id) + ); + } + if (canDeleteTeacher) { + rolePromises.push( + context.app + .service('roles') + .find({ query: { name: 'teacher' } }) + .then((r) => r.data[0]._id) + ); + } + const allowedRoles = await Promise.all(rolePromises); + + // there may not be any role in user.roles that is not in rolesToDelete + const roleQuery = { $nor: [{ roles: { $elemMatch: { $nin: allowedRoles } } }] }; + context.params.query = { $and: [context.params.query, roleQuery] }; + return context; +}; + +const enforceRoleHierarchyOnDelete = async (context) => { + if (context.id) return enforceRoleHierarchyOnDeleteSingle(context); + return enforceRoleHierarchyOnDeleteBulk(context); +}; + +/** + * Check that the authenticated user posseses the rights to create a user with the given roles. + * This is only checked for external requests. + * @param {*} context + */ +const enforceRoleHierarchyOnCreate = async (context) => { + const user = await context.app.service('users').get(context.params.account.userId, { query: { $populate: 'roles' } }); + + // superhero may create users with every role + if (user.roles.filter((u) => u.name === 'superhero').length > 0) { + return Promise.resolve(context); + } + + // created user has no role + if (!context.data || !context.data.roles) { + return Promise.resolve(context); + } + await Promise.all( + context.data.roles.map(async (roleId) => { + // Roles are given by ID or by name. + // For IDs we load the name from the DB. + // If it is not an ID we assume, it is a name. Invalid names are rejected in the switch anyways. + let roleName = ''; + if (!ObjectId.isValid(roleId)) { + roleName = roleId; + } else { + try { + const role = await context.app.service('roles').get(roleId); + roleName = role.name; + } catch (exception) { + return Promise.reject(new BadRequest('No such role exists')); + } + } + switch (roleName) { + case 'teacher': + if (!user.permissions.find((permission) => permission === 'TEACHER_CREATE')) { + return Promise.reject(new BadRequest('Your are not allowed to create a user with the given role')); + } + break; + case 'student': + if (!user.permissions.find((permission) => permission === 'STUDENT_CREATE')) { + return Promise.reject(new BadRequest('Your are not allowed to create a user with the given role')); + } + break; + case 'parent': + break; + default: + return Promise.reject(new BadRequest('Your are not allowed to create a user with the given role')); + } + return Promise.resolve(context); + }) + ); + + return Promise.resolve(context); +}; + +const generateRegistrationLink = async (context) => { + const { data, app } = context; + if (data.generateRegistrationLink === true) { + delete data.generateRegistrationLink; + if (!data.roles || data.roles.length > 1) { + throw new BadRequest('Roles must be exactly of length one if generateRegistrationLink=true is set.'); + } + const { hash } = await app + .service('/registrationlink') + // set account in params to context.parmas.account to reference the current user + .create({ + role: data.roles[0], + save: true, + patchUser: true, + host: SC_DOMAIN, + schoolId: data.schoolId, + toHash: data.email, + }) + .catch((err) => { + throw new GeneralError(`Can not create registrationlink. ${err}`); + }); + context.data.importHash = hash; + } +}; + +const sendRegistrationLink = async (context) => { + const { result, data, app } = context; + if (data.sendRegistration === true) { + delete data.sendRegistration; + await app.service('/users/mail/registrationLink').create({ + users: [result], + }); + } + return context; +}; + +const filterResult = async (context) => { + const userCallingHimself = context.id && ObjectId.equal(context.id, context.params.account.userId); + const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); + if (userCallingHimself || userIsSuperhero) { + return context; + } + + const allowedAttributes = [ + '_id', + 'roles', + 'schoolId', + 'firstName', + 'middleName', + 'lastName', + 'namePrefix', + 'nameSuffix', + 'discoverable', + 'fullName', + 'displayName', + 'avatarInitials', + 'avatarBackgroundColor', + 'outdatedSince', + ]; + return keep(...allowedAttributes)(context); +}; + +let roleCache = null; +const includeOnlySchoolRoles = async (context) => { + if (context.params && context.params.query) { + const userIsSuperhero = await hasRoleNoHook(context, context.params.account.userId, 'superhero'); + if (userIsSuperhero) { + return context; + } + + // todo: remove with static role service (SC-3731) + if (!Array.isArray(roleCache)) { + roleCache = ( + await context.app.service('roles').find({ + query: { + name: { $in: ['administrator', 'teacher', 'student'] }, + }, + paginate: false, + }) + ).map((r) => r._id); + } + const allowedRoles = roleCache; + + if (context.params.query.roles && context.params.query.roles.$in) { + // when querying for specific roles, filter them + context.params.query.roles.$in = context.params.query.roles.$in.filter((r) => + allowedRoles.some((a) => ObjectId.equal(r, a)) + ); + } else { + // otherwise, overwrite them with whitelist + context.params.query.roles = { + $in: allowedRoles, + }; + } + } + return context; +}; + +module.exports = { + mapRoleFilterQuery, + checkUnique, + checkUniqueEmail, + checkJwt, + checkUniqueAccount, + updateAccountUsername, + removeStudentFromClasses, + removeStudentFromCourses, + sanitizeData, + pinIsVerified, + protectImmutableAttributes, + securePatching, + decorateUser, + decorateAvatar, + decorateUsers, + handleClassId, + pushRemoveEvent, + enforceRoleHierarchyOnDelete, + enforceRoleHierarchyOnCreate, + filterResult, + generateRegistrationLink, + sendRegistrationLink, + includeOnlySchoolRoles, +}; diff --git a/test/services/user/hooks/userService.hooks.test.js b/test/services/user/hooks/userService.hooks.test.js index f779b6a4291..98a49c0594a 100644 --- a/test/services/user/hooks/userService.hooks.test.js +++ b/test/services/user/hooks/userService.hooks.test.js @@ -153,16 +153,18 @@ describe('generateRegistrationLink', () => { const expectedErrorMessage = 'Roles must be exactly of length one if generateRegistrationLink=true is set.'; - const getAppMock = (registrationlinkMock) => ({ - service: (service) => { - if (service === '/registrationlink') { - return { - create: async (data) => registrationlinkMock(data), - }; - } - throw new Error('unknown service'); - }, - }); + const getAppMock = (registrationlinkMock) => { + return { + service: (service) => { + if (service === '/registrationlink') { + return { + create: async (data) => registrationlinkMock(data), + }; + } + throw new Error('unknown service'); + }, + }; + }; it('throws an error if roles is not defined', async () => { const context = { @@ -439,7 +441,6 @@ describe('checkUniqueEmail', () => { const currentTs = Date.now(); const currentEmail = `current.${currentTs}@account.de`; - const updatedEmail = `Current.${currentTs}@Account.DE`; const changedEmail = `Changed.${currentTs}@Account.DE`; const mockUser = { firstName: 'Test', @@ -450,13 +451,14 @@ describe('checkUniqueEmail', () => { it('fails because of duplicate email', async () => { const expectedErrorMessage = `Die E-Mail Adresse ist bereits in Verwendung!`; - await testObjects.createTestUser({ email: currentEmail }); + const user = await testObjects.createTestUser(); + await app.service('nest-account-service').save({ username: user.email, password: 'password', userId: user._id }); const context = { app, data: { ...mockUser, - email: updatedEmail, + email: user.email, }, }; diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 3b1fedd050b..377a700b20b 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -8,9 +8,6 @@ const { ConfigModule } = require('@nestjs/config'); const { AccountApiModule } = require('../../dist/apps/server/modules/account/account-api.module'); const { AccountUc } = require('../../dist/apps/server/modules/account/api/account.uc'); const { AccountService } = require('../../dist/apps/server/modules/account/domain/services/account.service'); -const { - AccountValidationService, -} = require('../../dist/apps/server/modules/account/domain/services/account.validation.service'); const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/config/database.config'); const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); @@ -42,13 +39,11 @@ const setupNestServices = async (app) => { const orm = nestApp.get(MikroORM); const accountUc = nestApp.get(AccountUc); const accountService = nestApp.get(AccountService); - const accountValidationService = nestApp.get(AccountValidationService); const teamService = nestApp.get(TeamService); const systemRule = nestApp.get(SystemRule); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; - app.services['nest-account-validation-service'] = accountValidationService; app.services['nest-team-service'] = teamService; app.services['nest-system-rule'] = systemRule; app.services['nest-orm'] = orm; From 00322b685dacd52ca76b6f53d6675c2ceb94ca7e Mon Sep 17 00:00:00 2001 From: Majed Mak <132336669+MajedAlaitwniCap@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:08:21 +0200 Subject: [PATCH 05/35] EW-924 fix import and add markup for text elements --- .../import/common-cartridge-file-parser.ts | 2 +- .../import/common-cartridge-import.types.ts | 3 +++ .../common-cartridge-resource-factory.spec.ts | 17 +++++++++-------- .../import/common-cartridge-resource-factory.ts | 17 +++++++++++++---- ...ommon-cartridge-organization-visitor.spec.ts | 1 + .../service/common-cartridge-import.service.ts | 2 +- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/apps/server/src/modules/common-cartridge/import/common-cartridge-file-parser.ts b/apps/server/src/modules/common-cartridge/import/common-cartridge-file-parser.ts index 6e57b5b98a6..847dfa66f35 100644 --- a/apps/server/src/modules/common-cartridge/import/common-cartridge-file-parser.ts +++ b/apps/server/src/modules/common-cartridge/import/common-cartridge-file-parser.ts @@ -51,7 +51,7 @@ export class CommonCartridgeFileParser { public getResource(organization: CommonCartridgeOrganizationProps): CommonCartridgeResourceProps | undefined { this.checkOrganization(organization); - const resource = this.resourceFactory.create(organization); + const resource = this.resourceFactory.create(organization, this.options.inputFormat); return resource; } diff --git a/apps/server/src/modules/common-cartridge/import/common-cartridge-import.types.ts b/apps/server/src/modules/common-cartridge/import/common-cartridge-import.types.ts index 96152436f41..ce86755bb60 100644 --- a/apps/server/src/modules/common-cartridge/import/common-cartridge-import.types.ts +++ b/apps/server/src/modules/common-cartridge/import/common-cartridge-import.types.ts @@ -1,13 +1,16 @@ +import { InputFormat } from '@shared/domain/types'; import { CommonCartridgeResourceTypeV1P1 } from './common-cartridge-import.enums'; export type CommonCartridgeFileParserOptions = { maxSearchDepth: number; pathSeparator: string; + inputFormat: InputFormat; }; export const DEFAULT_FILE_PARSER_OPTIONS: CommonCartridgeFileParserOptions = { maxSearchDepth: 5, pathSeparator: '/', + inputFormat: InputFormat.RICH_TEXT_CK5, }; export type CommonCartridgeOrganizationProps = { diff --git a/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.spec.ts index 5bff23b9ce5..6f0d867681b 100644 --- a/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.spec.ts +++ b/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.spec.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker/locale/af_ZA'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { InputFormat } from '@shared/domain/types'; import AdmZip from 'adm-zip'; import { readFile } from 'node:fs/promises'; import { CommonCartridgeResourceTypeV1P1 } from './common-cartridge-import.enums'; @@ -81,7 +82,7 @@ describe('CommonCartridgeResourceFactory', () => { it('should create a web link resource', async () => { const { organizationProps } = await setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toStrictEqual({ type: CommonCartridgeResourceTypeV1P1.WEB_LINK, @@ -105,7 +106,7 @@ describe('CommonCartridgeResourceFactory', () => { it('should return undefined', () => { const { organizationProps } = setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toBeUndefined(); }); @@ -123,7 +124,7 @@ describe('CommonCartridgeResourceFactory', () => { it('should return undefined', () => { const { organizationProps } = setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toBeUndefined(); }); @@ -142,7 +143,7 @@ describe('CommonCartridgeResourceFactory', () => { it('should return undefined', () => { const { organizationProps } = setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toBeUndefined(); }); @@ -163,12 +164,12 @@ describe('CommonCartridgeResourceFactory', () => { it('should create a web content resource', () => { const { organizationProps } = setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toStrictEqual({ type: CommonCartridgeResourceTypeV1P1.WEB_CONTENT, title: organizationProps.title, - html: 'Content', + html: '

Content

', }); }); }); @@ -186,7 +187,7 @@ describe('CommonCartridgeResourceFactory', () => { it('should return an empty value', () => { const { organizationProps } = setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toStrictEqual({ type: CommonCartridgeResourceTypeV1P1.WEB_CONTENT, @@ -208,7 +209,7 @@ describe('CommonCartridgeResourceFactory', () => { it('should return undefined', () => { const { organizationProps } = setup(); - const result = sut.create(organizationProps); + const result = sut.create(organizationProps, InputFormat.RICH_TEXT_CK5); expect(result).toBeUndefined(); }); diff --git a/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.ts index a759e36f136..b8058b59f95 100644 --- a/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.ts +++ b/apps/server/src/modules/common-cartridge/import/common-cartridge-resource-factory.ts @@ -1,3 +1,5 @@ +import { sanitizeRichText } from '@shared/controller'; +import { InputFormat } from '@shared/domain/types'; import AdmZip from 'adm-zip'; import { JSDOM } from 'jsdom'; import { CommonCartridgeResourceTypeV1P1 } from './common-cartridge-import.enums'; @@ -11,7 +13,10 @@ import { export class CommonCartridgeResourceFactory { constructor(private readonly archive: AdmZip) {} - public create(organization: CommonCartridgeOrganizationProps): CommonCartridgeResourceProps | undefined { + public create( + organization: CommonCartridgeOrganizationProps, + inputFormat: InputFormat + ): CommonCartridgeResourceProps | undefined { if (!this.isValidOrganization(organization)) { return undefined; } @@ -23,7 +28,7 @@ export class CommonCartridgeResourceFactory { case CommonCartridgeResourceTypeV1P1.WEB_LINK: return this.createWebLinkResource(content, title); case CommonCartridgeResourceTypeV1P1.WEB_CONTENT: - return this.createWebContentResource(content, title); + return this.createWebContentResource(content, title, inputFormat); default: return undefined; } @@ -52,14 +57,18 @@ export class CommonCartridgeResourceFactory { }; } - private createWebContentResource(content: string, title: string): CommonCartridgeWebContentResourceProps | undefined { + private createWebContentResource( + content: string, + title: string, + inputFormat: InputFormat + ): CommonCartridgeWebContentResourceProps | undefined { const document = this.tryCreateDocument(content, 'text/html'); if (!document) { return undefined; } - const html = document.body.textContent?.trim() ?? ''; + const html = sanitizeRichText(document.body.innerHTML?.trim() ?? '', inputFormat); return { type: CommonCartridgeResourceTypeV1P1.WEB_CONTENT, diff --git a/apps/server/src/modules/common-cartridge/import/utils/common-cartridge-organization-visitor.spec.ts b/apps/server/src/modules/common-cartridge/import/utils/common-cartridge-organization-visitor.spec.ts index f9775a8e887..ed0b6e14ce8 100644 --- a/apps/server/src/modules/common-cartridge/import/utils/common-cartridge-organization-visitor.spec.ts +++ b/apps/server/src/modules/common-cartridge/import/utils/common-cartridge-organization-visitor.spec.ts @@ -28,6 +28,7 @@ describe('CommonCartridgeOrganizationVisitor', () => { const sut = new CommonCartridgeOrganizationVisitor(document, { maxSearchDepth: 1, pathSeparator: DEFAULT_FILE_PARSER_OPTIONS.pathSeparator, + inputFormat: DEFAULT_FILE_PARSER_OPTIONS.inputFormat, }); return { sut }; diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts index af55a6eb2a9..c57b7300510 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts @@ -161,7 +161,7 @@ export class CommonCartridgeImportService { const { title, height } = this.mapper.mapOrganizationToCard(cardProps, true); card.title = title; card.height = height; - await this.boardNodeService.addToParent(column, column); + await this.boardNodeService.addToParent(column, card); const cardElements = organizations.filter( (organization) => organization.pathDepth >= 3 && organization.path.startsWith(cardProps.path) From 4a10de7ea449f06081ba2f39c73ae06f395eca0b Mon Sep 17 00:00:00 2001 From: Max Bischof <106820326+bischofmax@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:53:59 +0200 Subject: [PATCH 06/35] BC-7113 - Activate etherpad (#5079) --- config/default.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/default.schema.json b/config/default.schema.json index 743bfe41711..5622b162ee8 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1120,7 +1120,7 @@ }, "FEATURE_COLUMN_BOARD_COLLABORATIVE_TEXT_EDITOR_ENABLED": { "type": "boolean", - "default": false, + "default": true, "description": "Enable collaborative text editor in column board." }, "FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED": { From cf58b6b3ebdacacfde538a8517477e4e76ba7f1b Mon Sep 17 00:00:00 2001 From: mkreuzkam-cap <144103168+mkreuzkam-cap@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:30:35 +0200 Subject: [PATCH 07/35] EW-917: Improve common-cartridge-import-service test. (#5081) --- .../common-cartridge-import.service.spec.ts | 224 ++++++++++++------ 1 file changed, 147 insertions(+), 77 deletions(-) diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts index 1967d3e51a8..6607bcb5784 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts @@ -1,8 +1,10 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { MikroORM } from '@mikro-orm/core'; import { Test, TestingModule } from '@nestjs/testing'; +import { InputFormat } from '@shared/domain/types'; import { setupEntities, userFactory } from '@shared/testing'; import { BoardNodeFactory, BoardNodeService } from '@src/modules/board'; +import { LinkElement, RichTextElement } from '@src/modules/board/domain'; import { readFile } from 'fs/promises'; import { CommonCartridgeImportMapper } from '../mapper/common-cartridge-import.mapper'; import { CommonCartridgeImportService } from './common-cartridge-import.service'; @@ -13,23 +15,42 @@ describe('CommonCartridgeImportService', () => { let moduleRef: TestingModule; let sut: CommonCartridgeImportService; let courseServiceMock: DeepMocked; - let boardNodeFactoryMock: DeepMocked; + let boardNodeFactory: BoardNodeFactory; let boardNodeServiceMock: DeepMocked; + const courseName = 'Test Kurs'; + + const board1Title = 'Test Thema'; + const board2Title = ''; + const board3Title = 'Spaltenboard 1'; + + const column1ofBoard1Title = 'Test Text'; + const column1ofBoard2Title = 'Test Aufgabe'; + const column1ofBoard3Title = 'Spalte 1'; + const column2ofBoard3Title = 'Spalte 2'; + const column3ofBoard3Title = 'Spalte 3'; + const column4ofBoard3Title = 'Spalte 4'; + + const emptyCardTitle = ''; + const card1Title = 'Karte 1'; + const card2Title = 'Karte 2'; + const card3Title = 'Karte 3'; + const card4Title = 'Karte 4'; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const objectContainingTitle = (title: string) => expect.objectContaining({ title }); + beforeEach(async () => { orm = await setupEntities(); moduleRef = await Test.createTestingModule({ providers: [ CommonCartridgeImportService, CommonCartridgeImportMapper, + BoardNodeFactory, { provide: CourseService, useValue: createMock(), }, - { - provide: BoardNodeFactory, - useValue: createMock(), - }, { provide: BoardNodeService, useValue: createMock(), @@ -39,7 +60,7 @@ describe('CommonCartridgeImportService', () => { sut = moduleRef.get(CommonCartridgeImportService); courseServiceMock = moduleRef.get(CourseService); - boardNodeFactoryMock = moduleRef.get(BoardNodeFactory); + boardNodeFactory = moduleRef.get(BoardNodeFactory); boardNodeServiceMock = moduleRef.get(BoardNodeService); }); @@ -56,67 +77,19 @@ describe('CommonCartridgeImportService', () => { expect(sut).toBeDefined(); }); - const setupEnvironment = async (filePath: string) => { - const user = userFactory.buildWithId(); - const buffer = await readFile(filePath); - - return { user, buffer }; - }; - describe('importFile', () => { - describe('when the common cartridge is valid', () => { - const setup = async () => - setupEnvironment('./apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc'); - - it('should create a course', async () => { - const { user, buffer } = await setup(); - - await sut.importFile(user, buffer); - - expect(courseServiceMock.create).toHaveBeenCalledTimes(1); - }); - - it('should create a column board', async () => { - const { user, buffer } = await setup(); - - await sut.importFile(user, buffer); - - expect(boardNodeFactoryMock.buildColumnBoard).toHaveBeenCalledTimes(14); - expect(boardNodeServiceMock.addRoot).toHaveBeenCalledTimes(14); - }); - - it('should create columns', async () => { - const { user, buffer } = await setup(); - - await sut.importFile(user, buffer); - - expect(boardNodeFactoryMock.buildColumn).toHaveBeenCalled(); - expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); - }); - - it('should create cards', async () => { - const { user, buffer } = await setup(); - - await sut.importFile(user, buffer); - - expect(boardNodeFactoryMock.buildCard).toHaveBeenCalled(); - expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); - }); - - it('should create elements', async () => { - const { user, buffer } = await setup(); + describe('when the common cartridge is a valid dbc course', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const buffer = await readFile('./apps/server/src/modules/common-cartridge/testing/assets/dbc_course.imscc'); - await sut.importFile(user, buffer); + const spyBuildColumnBoard = jest.spyOn(boardNodeFactory, 'buildColumnBoard'); + const spyBuildColumn = jest.spyOn(boardNodeFactory, 'buildColumn'); + const spyBuildCard = jest.spyOn(boardNodeFactory, 'buildCard'); + const spyBuildContentElement = jest.spyOn(boardNodeFactory, 'buildContentElement'); - expect(boardNodeFactoryMock.buildContentElement).toHaveBeenCalled(); - expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); - expect(boardNodeServiceMock.updateContent).toHaveBeenCalled(); - }); - }); - - describe('when the common cartridge is a valid dbc course', () => { - const setup = async () => - setupEnvironment('./apps/server/src/modules/common-cartridge/testing/assets/dbc_course.imscc'); + return { user, buffer, spyBuildColumnBoard, spyBuildColumn, spyBuildCard, spyBuildContentElement }; + }; it('should create a course', async () => { const { user, buffer } = await setup(); @@ -124,43 +97,140 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); expect(courseServiceMock.create).toHaveBeenCalledTimes(1); + expect(courseServiceMock.create).toHaveBeenCalledWith(expect.objectContaining({ name: courseName })); }); it('should create a column board', async () => { - const { user, buffer } = await setup(); + const { user, buffer, spyBuildColumnBoard } = await setup(); await sut.importFile(user, buffer); - expect(boardNodeFactoryMock.buildColumnBoard).toHaveBeenCalledTimes(3); - expect(boardNodeServiceMock.addRoot).toHaveBeenCalledTimes(3); + expect(spyBuildColumnBoard).toHaveBeenCalledTimes(3); + + expect(boardNodeServiceMock.addRoot).toHaveBeenCalledWith(objectContainingTitle(board1Title)); + expect(boardNodeServiceMock.addRoot).toHaveBeenCalledWith(objectContainingTitle(board2Title)); + expect(boardNodeServiceMock.addRoot).toHaveBeenCalledWith(objectContainingTitle(board3Title)); }); it('should create columns', async () => { - const { user, buffer } = await setup(); + const { user, buffer, spyBuildColumn } = await setup(); await sut.importFile(user, buffer); - expect(boardNodeFactoryMock.buildColumn).toHaveBeenCalled(); - expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); + expect(spyBuildColumn).toHaveBeenCalledTimes(6); + + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(board1Title), + objectContainingTitle(column1ofBoard1Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(board2Title), + objectContainingTitle(column1ofBoard2Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(board3Title), + objectContainingTitle(column1ofBoard3Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(board3Title), + objectContainingTitle(column2ofBoard3Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(board3Title), + objectContainingTitle(column3ofBoard3Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(board3Title), + objectContainingTitle(column4ofBoard3Title) + ); }); it('should create cards', async () => { - const { user, buffer } = await setup(); + const { user, buffer, spyBuildCard } = await setup(); await sut.importFile(user, buffer); - expect(boardNodeFactoryMock.buildCard).toHaveBeenCalled(); - expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); + expect(spyBuildCard).toHaveBeenCalledTimes(6); + + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(column1ofBoard1Title), + objectContainingTitle(emptyCardTitle) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(column1ofBoard2Title), + objectContainingTitle(emptyCardTitle) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(column1ofBoard3Title), + objectContainingTitle(card1Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(column2ofBoard3Title), + objectContainingTitle(card2Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(column3ofBoard3Title), + objectContainingTitle(card3Title) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(column4ofBoard3Title), + objectContainingTitle(card4Title) + ); }); it('should create elements', async () => { - const { user, buffer } = await setup(); + const { user, buffer, spyBuildContentElement } = await setup(); await sut.importFile(user, buffer); - expect(boardNodeFactoryMock.buildContentElement).toHaveBeenCalled(); - expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); - expect(boardNodeServiceMock.updateContent).toHaveBeenCalled(); + expect(spyBuildContentElement).toHaveBeenCalledTimes(6); + + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(emptyCardTitle), + expect.any(RichTextElement) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(card1Title), + expect.any(RichTextElement) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(card2Title), + expect.any(LinkElement) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(card3Title), + expect.any(LinkElement) + ); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalledWith( + objectContainingTitle(card4Title), + expect.any(RichTextElement) + ); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledTimes(6); + + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledWith(expect.any(RichTextElement), { + text: 'Test Text

Dies ist ein Textinhalt.

', + inputFormat: InputFormat.RICH_TEXT_CK5_SIMPLE, + }); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledWith(expect.any(RichTextElement), { + text: 'Test Aufgabe

', + inputFormat: InputFormat.RICH_TEXT_CK5_SIMPLE, + }); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledWith(expect.any(RichTextElement), { + text: '

Karteninhalt von Karte 1

', + inputFormat: InputFormat.RICH_TEXT_CK5_SIMPLE, + }); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledWith(expect.any(LinkElement), { + title: 'Example Domain', + url: 'https://www.example.org/', + }); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledWith(expect.any(LinkElement), { + title: 'Karte 3', + url: 'https://www.example.org/', + }); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalledWith(expect.any(RichTextElement), { + text: 'Example Domain', + inputFormat: 'richTextCk5Simple', + }); }); }); }); From ee670e4ed6d9432a2f57c460cc99b96e830d1142 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport <106819770+hoeppner-dataport@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:16:08 +0200 Subject: [PATCH 08/35] BC-7583 - fix bugs in socket implementation (#5086) Fixing two bugs that were introduced with the implementation of websocket-communication for boards. 1: When a new card gets created by clicking on the corresponding button, it gets created without an empty text field element. 2: When the user hits enter key while being in a card title, a new text field element should be created at the beginning of the card and the cursor should jump into it. --- .../board/gateway/board-collaboration.gateway.ts | 2 +- .../board/gateway/dto/create-card.message.param.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index a647be3ef08..93bffa3cf0e 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -169,7 +169,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { const emitter = await this.buildBoardSocketEmitter({ socket, id: data.columnId, action: 'create-card' }); const { userId } = this.getCurrentUser(socket); try { - const card = await this.columnUc.createCard(userId, data.columnId); + const card = await this.columnUc.createCard(userId, data.columnId, data.requiredEmptyElements); const newCard = CardResponseMapper.mapToResponse(card); const responsePayload = { diff --git a/apps/server/src/modules/board/gateway/dto/create-card.message.param.ts b/apps/server/src/modules/board/gateway/dto/create-card.message.param.ts index aecff720b5a..fc596d8bd2c 100644 --- a/apps/server/src/modules/board/gateway/dto/create-card.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/create-card.message.param.ts @@ -1,6 +1,17 @@ -import { IsMongoId } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsMongoId, IsOptional } from 'class-validator'; +import { ContentElementType } from '../../domain'; export class CreateCardMessageParams { @IsMongoId() columnId!: string; + + @IsEnum(ContentElementType, { each: true }) + @IsOptional() + @ApiPropertyOptional({ + required: false, + isArray: true, + enum: ContentElementType, + }) + requiredEmptyElements?: ContentElementType[]; } From 18c6fa702ab3741dd7e9e971f06090526c34c266 Mon Sep 17 00:00:00 2001 From: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:50:28 +0200 Subject: [PATCH 09/35] BC-7585 - tldraw-console, Nest can't resolve dependencies (#5087) --- .../modules/tldraw/tldraw-console.module.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/server/src/modules/tldraw/tldraw-console.module.ts b/apps/server/src/modules/tldraw/tldraw-console.module.ts index 171505bd510..fb3260def02 100644 --- a/apps/server/src/modules/tldraw/tldraw-console.module.ts +++ b/apps/server/src/modules/tldraw/tldraw-console.module.ts @@ -1,18 +1,19 @@ +import { ConsoleWriterModule } from '@infra/console'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; +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 { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME } from '@src/config'; +import { DB_PASSWORD, DB_USERNAME, createConfigModuleOptions } from '@src/config'; +import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; -import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; -import { RabbitMQWrapperModule } from '@infra/rabbitmq'; -import { ConsoleWriterModule } from '@infra/console'; import { ConsoleModule } from 'nestjs-console'; import { FilesStorageClientModule } from '../files-storage-client'; -import { config, TLDRAW_DB_URL } from './config'; +import { TLDRAW_DB_URL, config } from './config'; import { TldrawDrawing } from './entities'; -import { TldrawFilesStorageAdapterService } from './service'; -import { TldrawRepo, YMongodb } from './repo'; import { TldrawFilesConsole } from './job'; +import { TldrawRepo, YMongodb } from './repo'; +import { TldrawFilesStorageAdapterService } from './service'; import { TldrawDeleteFilesUc } from './uc'; const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { @@ -28,6 +29,7 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { RabbitMQWrapperModule, FilesStorageClientModule, LoggerModule, + CoreModule, MikroOrmModule.forRoot({ ...defaultMikroOrmOptions, type: 'mongo', From ee49c8043bfcd10449b4d1074e4f6c6448f50a79 Mon Sep 17 00:00:00 2001 From: MBergCap <111343628+MBergCap@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:53:52 +0200 Subject: [PATCH 10/35] N21-1564 add seed data (#5085) --- backup/setup/external-tools.json | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/backup/setup/external-tools.json b/backup/setup/external-tools.json index 7130b67a48b..f9e53c37498 100644 --- a/backup/setup/external-tools.json +++ b/backup/setup/external-tools.json @@ -113,6 +113,33 @@ "isDeactivated": false, "restrictToContexts": [] }, + { + "_id": { + "$oid": "667e4fe648ea6a22a5474359" + }, + "createdAt": { + "$date": { + "$numberLong": "1682589075592" + } + }, + "updatedAt": { + "$date": { + "$numberLong": "1682589075592" + } + }, + "name": "CY Test Tool Course Restriction", + "url": "https://google.de/", + "config_type": "basic", + "config_baseUrl": "https://google.de/", + "parameters": [], + "isHidden": false, + "openNewTab": true, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [ + "course" + ] + }, { "_id": { "$oid": "644a4593d0a8301e6cf25d86" @@ -140,6 +167,60 @@ "board-element" ] }, + { + "_id": { + "$oid": "667e50f6162707ce02b9ac02" + }, + "createdAt": { + "$date": { + "$numberLong": "1682589075592" + } + }, + "updatedAt": { + "$date": { + "$numberLong": "1682589075592" + } + }, + "name": "CY Test Tool Media-Board Restriction", + "url": "https://google.de/", + "config_type": "basic", + "config_baseUrl": "https://google.de/", + "parameters": [], + "isHidden": false, + "openNewTab": true, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [ + "media-board" + ] + }, + { + "_id": { + "$oid": "667e52a4162707ce02b9ac04" + }, + "createdAt": { + "$date": { + "$numberLong": "1682589075592" + } + }, + "updatedAt": { + "$date": { + "$numberLong": "1682589075592" + } + }, + "name": "CY Test Tool All Restrictions", + "url": "https://google.de/", + "config_type": "basic", + "config_baseUrl": "https://google.de/", + "parameters": [], + "isHidden": false, + "openNewTab": true, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [ + "course","board-element","media-board" + ] + }, { "_id": { "$oid": "647de247cf6a427b9d39e5b1" From c8f2fc19e84dfd643a0157a693dc3feef6687cc8 Mon Sep 17 00:00:00 2001 From: Max <53796487+dyedwiper@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:01:54 +0200 Subject: [PATCH 11/35] BC-3836 Save last login of account in database (#5077) --- .../src/modules/account/domain/account.ts | 9 ++++ .../account/domain/entity/account.entity.ts | 5 +++ .../services/account-db.service.spec.ts | 45 ++++++++++++------- .../domain/services/account-db.service.ts | 8 ++++ .../domain/services/account.service.spec.ts | 10 +++++ .../domain/services/account.service.ts | 4 ++ .../mapper/account-do-to-entity.mapper.ts | 1 + .../mapper/account-entity-to-do.mapper.ts | 1 + .../services/authentication.service.spec.ts | 8 ++++ .../services/authentication.service.ts | 4 ++ .../authentication/uc/login.uc.spec.ts | 8 ++++ .../src/modules/authentication/uc/login.uc.ts | 2 + .../authentication/strategies/TSPStrategy.js | 4 ++ 13 files changed, 93 insertions(+), 16 deletions(-) diff --git a/apps/server/src/modules/account/domain/account.ts b/apps/server/src/modules/account/domain/account.ts index 82153ffee4a..2b15037c12c 100644 --- a/apps/server/src/modules/account/domain/account.ts +++ b/apps/server/src/modules/account/domain/account.ts @@ -13,6 +13,7 @@ export interface AccountProps extends AuthorizableObject { password?: string; token?: string; credentialHash?: string; + lastLogin?: Date; lasttriedFailedLogin?: Date; expiresAt?: Date; activated?: boolean; @@ -73,6 +74,14 @@ export class Account extends DomainObject { return this.props.credentialHash; } + public get lastLogin(): Date | undefined { + return this.props.lastLogin; + } + + public set lastLogin(lastLogin: Date | undefined) { + this.props.lastLogin = lastLogin; + } + public get lasttriedFailedLogin(): Date | undefined { return this.props.lasttriedFailedLogin; } diff --git a/apps/server/src/modules/account/domain/entity/account.entity.ts b/apps/server/src/modules/account/domain/entity/account.entity.ts index 1c82e085054..736e8897431 100644 --- a/apps/server/src/modules/account/domain/entity/account.entity.ts +++ b/apps/server/src/modules/account/domain/entity/account.entity.ts @@ -26,6 +26,10 @@ export class AccountEntity extends BaseEntityWithTimestamps { @Property({ nullable: true }) systemId?: ObjectId; + @Property({ nullable: true }) + @Index() + lastLogin?: Date; + @Property({ nullable: true }) lasttriedFailedLogin?: Date; @@ -47,6 +51,7 @@ export class AccountEntity extends BaseEntityWithTimestamps { this.userId = props.userId; this.systemId = props.systemId; this.lasttriedFailedLogin = props.lasttriedFailedLogin; + this.lastLogin = props.lastLogin; this.expiresAt = props.expiresAt; this.activated = props.activated; this.deactivatedAt = props.deactivatedAt; diff --git a/apps/server/src/modules/account/domain/services/account-db.service.spec.ts b/apps/server/src/modules/account/domain/services/account-db.service.spec.ts index 5ea9543837a..4b5911267a8 100644 --- a/apps/server/src/modules/account/domain/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account-db.service.spec.ts @@ -714,27 +714,40 @@ describe('AccountDbService', () => { }); }); + describe('updateLastLogin', () => { + const setup = () => { + const mockTeacherAccount = accountDoFactory.build(); + const theNewDate = new Date(); + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + + return { mockTeacherAccount, theNewDate }; + }; + + it('should update last tried failed login', async () => { + const { mockTeacherAccount, theNewDate } = setup(); + + const ret = await accountService.updateLastLogin(mockTeacherAccount.id, theNewDate); + + expect(ret.lastLogin).toEqual(theNewDate); + }); + }); + describe('updateLastTriedFailedLogin', () => { - describe('when update last failed Login', () => { - const setup = () => { - const mockTeacherAccount = accountDoFactory.build(); - const theNewDate = new Date(); + const setup = () => { + const mockTeacherAccount = accountDoFactory.build(); + const theNewDate = new Date(); - accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountRepo.findById.mockResolvedValue(mockTeacherAccount); - return { mockTeacherAccount, theNewDate }; - }; + return { mockTeacherAccount, theNewDate }; + }; - it('should update last tried failed login', async () => { - const { mockTeacherAccount, theNewDate } = setup(); - const ret = await accountService.updateLastTriedFailedLogin(mockTeacherAccount.id, theNewDate); + it('should update last tried failed login', async () => { + const { mockTeacherAccount, theNewDate } = setup(); + const ret = await accountService.updateLastTriedFailedLogin(mockTeacherAccount.id, theNewDate); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - ...mockTeacherAccount.getProps(), - lasttriedFailedLogin: theNewDate, - }); - }); + expect(ret.lasttriedFailedLogin).toEqual(theNewDate); }); }); diff --git a/apps/server/src/modules/account/domain/services/account-db.service.ts b/apps/server/src/modules/account/domain/services/account-db.service.ts index b95ddafb1f0..36df25fb82b 100644 --- a/apps/server/src/modules/account/domain/services/account-db.service.ts +++ b/apps/server/src/modules/account/domain/services/account-db.service.ts @@ -64,6 +64,14 @@ export class AccountServiceDb extends AbstractAccountService { return account; } + async updateLastLogin(accountId: EntityId, lastLogin: Date): Promise { + const internalId = await this.getInternalId(accountId); + const account = await this.accountRepo.findById(internalId); + account.lastLogin = lastLogin; + await this.accountRepo.save(account); + return account; + } + async updateLastTriedFailedLogin(accountId: EntityId, lastTriedFailedLogin: Date): Promise { const internalId = await this.getInternalId(accountId); const account = await this.accountRepo.findById(internalId); diff --git a/apps/server/src/modules/account/domain/services/account.service.spec.ts b/apps/server/src/modules/account/domain/services/account.service.spec.ts index cd2cbeea0e7..77d9d7ebf35 100644 --- a/apps/server/src/modules/account/domain/services/account.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account.service.spec.ts @@ -579,6 +579,16 @@ describe('AccountService', () => { }); }); + describe('updateLastLogin', () => { + it('should call updateLastLogin in accountServiceDb', async () => { + const someId = new ObjectId().toHexString(); + + await accountService.updateLastLogin(someId, new Date()); + + expect(accountServiceDb.updateLastLogin).toHaveBeenCalledTimes(1); + }); + }); + describe('updateLastTriedFailedLogin', () => { describe('When calling updateLastTriedFailedLogin in accountService', () => { it('should call updateLastTriedFailedLogin in accountServiceDb', async () => { diff --git a/apps/server/src/modules/account/domain/services/account.service.ts b/apps/server/src/modules/account/domain/services/account.service.ts index 042ab5a0f78..877332b5a53 100644 --- a/apps/server/src/modules/account/domain/services/account.service.ts +++ b/apps/server/src/modules/account/domain/services/account.service.ts @@ -348,6 +348,10 @@ export class AccountService extends AbstractAccountService implements DeletionSe return new Account({ ...ret.getProps(), idmReferenceId: idmAccount?.idmReferenceId }); } + async updateLastLogin(accountId: string, lastLogin: Date): Promise { + await this.accountDb.updateLastLogin(accountId, lastLogin); + } + async updateLastTriedFailedLogin(accountId: string, lastTriedFailedLogin: Date): Promise { const ret = await this.accountDb.updateLastTriedFailedLogin(accountId, lastTriedFailedLogin); const idmAccount = await this.executeIdmMethod(async () => { diff --git a/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts b/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts index 173e7e98d72..bf2bd4f5b47 100644 --- a/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts +++ b/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts @@ -10,6 +10,7 @@ export class AccountDoToEntityMapper { activated: account.activated, credentialHash: account.credentialHash, expiresAt: account.expiresAt, + lastLogin: account.lastLogin, lasttriedFailedLogin: account.lasttriedFailedLogin, password: account.password, systemId: account.systemId ? new ObjectId(account.systemId) : undefined, diff --git a/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts b/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts index 59b514840cf..000ece5001a 100644 --- a/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts +++ b/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts @@ -13,6 +13,7 @@ export class AccountEntityToDoMapper { activated: account.activated, credentialHash: account.credentialHash, expiresAt: account.expiresAt, + lastLogin: account.lastLogin, lasttriedFailedLogin: account.lasttriedFailedLogin, password: account.password, systemId: account.systemId?.toString(), diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 51f10a7109a..32d6850f243 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -179,6 +179,14 @@ describe('AuthenticationService', () => { }); }); + describe('updateLastLogin', () => { + it('should call accountService to update last login', async () => { + await authenticationService.updateLastLogin('mockAccountId'); + + expect(accountService.updateLastLogin).toHaveBeenCalledWith('mockAccountId', expect.any(Date)); + }); + }); + describe('normalizeUsername', () => { describe('when a username is entered', () => { it('should trim username', () => { diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index 4a2b816b1e9..124a4c419b8 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -79,6 +79,10 @@ export class AuthenticationService { } } + async updateLastLogin(accountId: string): Promise { + await this.accountService.updateLastLogin(accountId, new Date()); + } + async updateLastTriedFailedLogin(id: string): Promise { await this.accountService.updateLastTriedFailedLogin(id, new Date()); } diff --git a/apps/server/src/modules/authentication/uc/login.uc.spec.ts b/apps/server/src/modules/authentication/uc/login.uc.spec.ts index c0f1d924876..4b0d356402a 100644 --- a/apps/server/src/modules/authentication/uc/login.uc.spec.ts +++ b/apps/server/src/modules/authentication/uc/login.uc.spec.ts @@ -63,6 +63,14 @@ describe('LoginUc', () => { }); }); + it('should call updateLastLogin', async () => { + const { userInfo } = setup(); + + await loginUc.getLoginData(userInfo); + + expect(authenticationService.updateLastLogin).toHaveBeenCalledWith(userInfo.accountId); + }); + it('should return a loginDto', async () => { const { userInfo, loginDto } = setup(); diff --git a/apps/server/src/modules/authentication/uc/login.uc.ts b/apps/server/src/modules/authentication/uc/login.uc.ts index 80ab89ca49a..a676e0d79d3 100644 --- a/apps/server/src/modules/authentication/uc/login.uc.ts +++ b/apps/server/src/modules/authentication/uc/login.uc.ts @@ -14,6 +14,8 @@ export class LoginUc { const accessTokenDto: LoginDto = await this.authService.generateJwt(createJwtPayload); + await this.authService.updateLastLogin(userInfo.accountId); + const loginDto: LoginDto = new LoginDto({ accessToken: accessTokenDto.accessToken, }); diff --git a/src/services/authentication/strategies/TSPStrategy.js b/src/services/authentication/strategies/TSPStrategy.js index e615aa56857..d856578df0f 100644 --- a/src/services/authentication/strategies/TSPStrategy.js +++ b/src/services/authentication/strategies/TSPStrategy.js @@ -177,6 +177,10 @@ class TSPStrategy extends AuthenticationBaseStrategy { // find account and generate JWT payload const account = await app.service('nest-account-service').findByUserId(user._id.toString()); account._id = account.id; + + const now = new Date(); + await app.service('nest-account-service').updateLastLogin(account.id, now); + const { entity } = this.configuration; return { authentication: { strategy: this.name }, From 0efdae234006f88eff22508204d6d84617c5f2a9 Mon Sep 17 00:00:00 2001 From: mamutmk5 <3045922+mamutmk5@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:09:24 +0200 Subject: [PATCH 12/35] BC-7592 - remove double FileStorageDeployment Ansible job (#5090) --- ansible/roles/schulcloud-server-core/tasks/main.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ansible/roles/schulcloud-server-core/tasks/main.yml b/ansible/roles/schulcloud-server-core/tasks/main.yml index 06982d218fe..dcc0a88cd79 100644 --- a/ansible/roles/schulcloud-server-core/tasks/main.yml +++ b/ansible/roles/schulcloud-server-core/tasks/main.yml @@ -113,12 +113,6 @@ namespace: "{{ NAMESPACE }}" template: api-files-deployment.yml.j2 - - name: FileStorageDeployment - kubernetes.core.k8s: - kubeconfig: ~/.kube/config - namespace: "{{ NAMESPACE }}" - template: api-files-deployment.yml.j2 - - name: File Storage Ingress kubernetes.core.k8s: kubeconfig: ~/.kube/config From c88e496c53114f8a349db9c859b553071c3a146f Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Tue, 2 Jul 2024 08:40:59 +0200 Subject: [PATCH 13/35] BC-7236 - refactor how to get rootid in socket gateway (#5080) For any action that is executed on a board we have to find out who needs to get the resulting update. Socket.io organizes users in rooms. For the board - all clients on the same board are in the same room. The room name is based on the boardid. So we always need to find out the rootId of the boardNode the action was executed on. For example: if a card title was rename, the card is the boardNode-object and has the rootId of the board the card is placed on. *Until now*: the rootId was determined by (re)using the BoardAuthorisableService *Now*: the rootId can be taken directly from the boardNode (due to boardPersisting-refactorings of Team MilkyWay) which reduces the number of queries that need to be executed --------- Co-authored-by: hoeppner-dataport --- .../gateway/board-collaboration.gateway.ts | 158 ++++++++---------- .../src/modules/board/gateway/dto/index.ts | 39 ++++- apps/server/src/modules/board/uc/board.uc.ts | 12 +- .../src/modules/board/uc/card.uc.spec.ts | 1 + apps/server/src/modules/board/uc/card.uc.ts | 14 +- .../src/modules/board/uc/column.uc.spec.ts | 1 + apps/server/src/modules/board/uc/column.uc.ts | 11 +- .../src/modules/board/uc/element.uc.spec.ts | 1 + .../server/src/modules/board/uc/element.uc.ts | 4 +- 9 files changed, 142 insertions(+), 99 deletions(-) diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index 93bffa3cf0e..a04045d0b53 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -10,6 +10,7 @@ import { WsException, } from '@nestjs/websockets'; import { Server } from 'socket.io'; +import { EntityId } from '@shared/domain/types'; import { BoardResponseMapper, CardResponseMapper, @@ -18,29 +19,27 @@ import { } from '../controller/mapper'; import { MetricsService } from '../metrics/metrics.service'; import { TrackExecutionTime } from '../metrics/track-execution-time.decorator'; -import { BoardNodeAuthorizableService } from '../service'; import { BoardUc, CardUc, ColumnUc, ElementUc } from '../uc'; -import { - CreateCardMessageParams, - DeleteColumnMessageParams, - MoveCardMessageParams, - UpdateColumnTitleMessageParams, -} from './dto'; -import BoardCollaborationConfiguration from './dto/board-collaboration-config'; +import { CreateCardMessageParams } from './dto/create-card.message.param'; import { CreateColumnMessageParams } from './dto/create-column.message.param'; import { CreateContentElementMessageParams } from './dto/create-content-element.message.param'; import { DeleteBoardMessageParams } from './dto/delete-board.message.param'; import { DeleteCardMessageParams } from './dto/delete-card.message.param'; import { DeleteContentElementMessageParams } from './dto/delete-content-element.message.param'; +import { DeleteColumnMessageParams } from './dto/delete-column.message.param'; import { FetchBoardMessageParams } from './dto/fetch-board.message.param'; import { FetchCardsMessageParams } from './dto/fetch-cards.message.param'; +import { MoveCardMessageParams } from './dto/move-card.message.param'; import { MoveColumnMessageParams } from './dto/move-column.message.param'; import { MoveContentElementMessageParams } from './dto/move-content-element.message.param'; import { UpdateBoardTitleMessageParams } from './dto/update-board-title.message.param'; import { UpdateBoardVisibilityMessageParams } from './dto/update-board-visibility.message.param'; import { UpdateCardHeightMessageParams } from './dto/update-card-height.message.param'; import { UpdateCardTitleMessageParams } from './dto/update-card-title.message.param'; +import { UpdateColumnTitleMessageParams } from './dto/update-column-title.message.param'; import { UpdateContentElementMessageParams } from './dto/update-content-element.message.param'; +import BoardCollaborationConfiguration from './dto/board-collaboration-config'; +import { AnyBoardNode } from '../domain'; @UsePipes(new WsValidationPipe()) @WebSocketGateway(BoardCollaborationConfiguration.websocket) @@ -56,8 +55,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { private readonly columnUc: ColumnUc, private readonly cardUc: CardUc, private readonly elementUc: ElementUc, - private readonly metricsService: MetricsService, - private readonly authorizableService: BoardNodeAuthorizableService // to be removed + private readonly metricsService: MetricsService ) {} trackExecutionTime(methodName: string, executionTimeMs: number) { @@ -88,12 +86,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('delete-board-request') @UseRequestContext() async deleteBoard(socket: Socket, data: DeleteBoardMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.boardId, action: 'delete-board' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-board' }); const { userId } = this.getCurrentUser(socket); try { - await this.boardUc.deleteBoard(userId, data.boardId); - - await emitter.emitToClientAndRoom(data); + const board = await this.boardUc.deleteBoard(userId, data.boardId); + emitter.emitToClientAndRoom(data, board); } catch (err) { emitter.emitFailure(data); } @@ -104,12 +101,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async updateBoardTitle(socket: Socket, data: UpdateBoardTitleMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.boardId, action: 'update-board-title' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-title' }); const { userId } = this.getCurrentUser(socket); try { - await this.boardUc.updateBoardTitle(userId, data.boardId, data.newTitle); - - await emitter.emitToClientAndRoom(data); + const board = await this.boardUc.updateBoardTitle(userId, data.boardId, data.newTitle); + emitter.emitToClientAndRoom(data, board); } catch (err) { emitter.emitFailure(data); } @@ -120,12 +116,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async updateCardTitle(socket: Socket, data: UpdateCardTitleMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.cardId, action: 'update-card-title' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-title' }); const { userId } = this.getCurrentUser(socket); try { - await this.cardUc.updateCardTitle(userId, data.cardId, data.newTitle); - - await emitter.emitToClientAndRoom(data); + const card = await this.cardUc.updateCardTitle(userId, data.cardId, data.newTitle); + emitter.emitToClientAndRoom(data, card); } catch (err) { emitter.emitFailure(data); } @@ -135,12 +130,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('update-card-height-request') @UseRequestContext() async updateCardHeight(socket: Socket, data: UpdateCardHeightMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.cardId, action: 'update-card-height' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-height' }); const { userId } = this.getCurrentUser(socket); try { - await this.cardUc.updateCardHeight(userId, data.cardId, data.newHeight); - - await emitter.emitToClientAndRoom(data); + const card = await this.cardUc.updateCardHeight(userId, data.cardId, data.newHeight); + emitter.emitToClientAndRoom(data, card); } catch (err) { emitter.emitFailure(data); } @@ -150,12 +144,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('delete-card-request') @UseRequestContext() async deleteCard(socket: Socket, data: DeleteCardMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.cardId, action: 'delete-card' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-card' }); const { userId } = this.getCurrentUser(socket); try { - await this.cardUc.deleteCard(userId, data.cardId); - - await emitter.emitToClientAndRoom(data); + const rootId = await this.cardUc.deleteCard(userId, data.cardId); + emitter.emitToClientAndRoom(data, rootId); } catch (err) { emitter.emitFailure(data); } @@ -166,7 +159,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async createCard(socket: Socket, data: CreateCardMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.columnId, action: 'create-card' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-card' }); const { userId } = this.getCurrentUser(socket); try { const card = await this.columnUc.createCard(userId, data.columnId, data.requiredEmptyElements); @@ -177,7 +170,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { newCard, }; - await emitter.emitToClientAndRoom(responsePayload); + emitter.emitToClientAndRoom(responsePayload, card); } catch (err) { emitter.emitFailure(data); } @@ -187,7 +180,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('create-column-request') @UseRequestContext() async createColumn(socket: Socket, data: CreateColumnMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.boardId, action: 'create-column' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-column' }); const { userId } = this.getCurrentUser(socket); try { const column = await this.boardUc.createColumn(userId, data.boardId); @@ -197,7 +190,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { ...data, newColumn, }; - await emitter.emitToClientAndRoom(responsePayload); + emitter.emitToClientAndRoom(responsePayload, column); // payload needs to be returned to allow the client to do sequential operation // of createColumn and move the card into that column @@ -212,13 +205,13 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async fetchBoard(socket: Socket, data: FetchBoardMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.boardId, action: 'fetch-board' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'fetch-board' }); const { userId } = this.getCurrentUser(socket); try { const board = await this.boardUc.findBoard(userId, data.boardId); - const responsePayload = BoardResponseMapper.mapToResponse(board); - await emitter.emitToClient(responsePayload); + await emitter.joinRoom(board); + emitter.emitSuccess(responsePayload); } catch (err) { emitter.emitFailure(data); } @@ -228,12 +221,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('move-card-request') @UseRequestContext() async moveCard(socket: Socket, data: MoveCardMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.cardId, action: 'move-card' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-card' }); const { userId } = this.getCurrentUser(socket); try { - await this.columnUc.moveCard(userId, data.cardId, data.toColumnId, data.newIndex); - - await emitter.emitToClientAndRoom(data); + const card = await this.columnUc.moveCard(userId, data.cardId, data.toColumnId, data.newIndex); + emitter.emitToClientAndRoom(data, card); } catch (err) { emitter.emitFailure(data); } @@ -243,12 +235,16 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('move-column-request') @UseRequestContext() async moveColumn(socket: Socket, data: MoveColumnMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.targetBoardId, action: 'move-column' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-column' }); const { userId } = this.getCurrentUser(socket); try { - await this.boardUc.moveColumn(userId, data.columnMove.columnId, data.targetBoardId, data.columnMove.addedIndex); - - await emitter.emitToClientAndRoom(data); + const column = await this.boardUc.moveColumn( + userId, + data.columnMove.columnId, + data.targetBoardId, + data.columnMove.addedIndex + ); + emitter.emitToClientAndRoom(data, column); } catch (err) { emitter.emitFailure(data); } @@ -259,12 +255,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async updateColumnTitle(socket: Socket, data: UpdateColumnTitleMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.columnId, action: 'update-column-title' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-column-title' }); const { userId } = this.getCurrentUser(socket); try { - await this.columnUc.updateColumnTitle(userId, data.columnId, data.newTitle); - - await emitter.emitToClientAndRoom(data); + const column = await this.columnUc.updateColumnTitle(userId, data.columnId, data.newTitle); + emitter.emitToClientAndRoom(data, column); } catch (err) { emitter.emitFailure(data); } @@ -274,12 +269,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('update-board-visibility-request') @UseRequestContext() async updateBoardVisibility(socket: Socket, data: UpdateBoardVisibilityMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.boardId, action: 'update-board-visibility' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-visibility' }); const { userId } = this.getCurrentUser(socket); try { - await this.boardUc.updateVisibility(userId, data.boardId, data.isVisible); - - await emitter.emitToClientAndRoom(data); + const board = await this.boardUc.updateVisibility(userId, data.boardId, data.isVisible); + emitter.emitToClientAndRoom(data, board); } catch (err) { emitter.emitFailure(data); } @@ -289,12 +283,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('delete-column-request') @UseRequestContext() async deleteColumn(socket: Socket, data: DeleteColumnMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.columnId, action: 'delete-column' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-column' }); const { userId } = this.getCurrentUser(socket); try { - await this.columnUc.deleteColumn(userId, data.columnId); - - await emitter.emitToClientAndRoom(data); + const rootId = await this.columnUc.deleteColumn(userId, data.columnId); + emitter.emitToClientAndRoom(data, rootId); } catch (err) { emitter.emitFailure(data); } @@ -305,13 +298,13 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async fetchCards(socket: Socket, data: FetchCardsMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.cardIds[0], action: 'fetch-card' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'fetch-card' }); const { userId } = this.getCurrentUser(socket); try { const cards = await this.cardUc.findCards(userId, data.cardIds); const cardResponses = cards.map((card) => CardResponseMapper.mapToResponse(card)); - await emitter.emitToClient({ cards: cardResponses, isOwnAction: false }); + emitter.emitSuccess({ cards: cardResponses }); } catch (err) { emitter.emitFailure(data); } @@ -321,7 +314,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('create-element-request') @UseRequestContext() async createElement(socket: Socket, data: CreateContentElementMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.cardId, action: 'create-element' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-element' }); const { userId } = this.getCurrentUser(socket); try { const element = await this.cardUc.createElement(userId, data.cardId, data.type, data.toPosition); @@ -330,7 +323,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { ...data, newElement: ContentElementResponseFactory.mapToResponse(element), }; - await emitter.emitToClientAndRoom(responsePayload); + emitter.emitToClientAndRoom(responsePayload, element); } catch (err) { emitter.emitFailure(data); } @@ -341,12 +334,11 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @TrackExecutionTime() @UseRequestContext() async updateElement(socket: Socket, data: UpdateContentElementMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.elementId, action: 'update-element' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-element' }); const { userId } = this.getCurrentUser(socket); try { - await this.elementUc.updateElement(userId, data.elementId, data.data.content); - - await emitter.emitToClientAndRoom(data); + const element = await this.elementUc.updateElement(userId, data.elementId, data.data.content); + emitter.emitToClientAndRoom(data, element); } catch (err) { emitter.emitFailure(data); } @@ -356,12 +348,12 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('delete-element-request') @UseRequestContext() async deleteElement(socket: Socket, data: DeleteContentElementMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.elementId, action: 'delete-element' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-element' }); const { userId } = this.getCurrentUser(socket); try { - await this.elementUc.deleteElement(userId, data.elementId); - await emitter.emitToClientAndRoom(data); + const rootId = await this.elementUc.deleteElement(userId, data.elementId); + emitter.emitToClientAndRoom(data, rootId); } catch (err) { emitter.emitFailure(data); } @@ -371,28 +363,33 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { @SubscribeMessage('move-element-request') @UseRequestContext() async moveElement(socket: Socket, data: MoveContentElementMessageParams) { - const emitter = await this.buildBoardSocketEmitter({ socket, id: data.elementId, action: 'move-element' }); + const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-element' }); const { userId } = this.getCurrentUser(socket); try { - await this.cardUc.moveElement(userId, data.elementId, data.toCardId, data.toPosition); - await emitter.emitToClientAndRoom(data); + const element = await this.cardUc.moveElement(userId, data.elementId, data.toCardId, data.toPosition); + emitter.emitToClientAndRoom(data, element); } catch (err) { emitter.emitFailure(data); } await this.updateRoomsAndUsersMetrics(socket); } - private async buildBoardSocketEmitter({ socket, id, action }: { socket: Socket; id: string; action: string }) { - const rootId = await this.getRootIdForId(id); - const room = `board_${rootId}`; + private buildBoardSocketEmitter({ socket, action }: { socket: Socket; action: string }) { + const getRoomName = (boardNode: AnyBoardNode | EntityId) => { + const rootId = typeof boardNode === 'string' ? boardNode : boardNode.rootId; + return `board_${rootId}`; + }; return { - async emitToClient(data: object) { + async joinRoom(boardNode: AnyBoardNode) { + const room = getRoomName(boardNode); await socket.join(room); + }, + emitSuccess(data: object) { socket.emit(`${action}-success`, { ...data, isOwnAction: true }); }, - async emitToClientAndRoom(data: object) { - await socket.join(room); + emitToClientAndRoom(data: object, boardNodeOrRootId: AnyBoardNode | EntityId) { + const room = getRoomName(boardNodeOrRootId); socket.to(room).emit(`${action}-success`, { ...data, isOwnAction: false }); socket.emit(`${action}-success`, { ...data, isOwnAction: true }); }, @@ -401,11 +398,4 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { }, }; } - - private async getRootIdForId(id: string) { - const authorizable = await this.authorizableService.findById(id); - const rootId = authorizable.rootNode.id; - - return rootId; - } } diff --git a/apps/server/src/modules/board/gateway/dto/index.ts b/apps/server/src/modules/board/gateway/dto/index.ts index 16c8d1dd64c..35d8c364514 100644 --- a/apps/server/src/modules/board/gateway/dto/index.ts +++ b/apps/server/src/modules/board/gateway/dto/index.ts @@ -1,6 +1,39 @@ -import { MoveCardMessageParams } from './move-card.message.param'; import { CreateCardMessageParams } from './create-card.message.param'; -import { UpdateColumnTitleMessageParams } from './update-column-title.message.param'; +import { CreateColumnMessageParams } from './create-column.message.param'; +import { CreateContentElementMessageParams } from './create-content-element.message.param'; +import { DeleteBoardMessageParams } from './delete-board.message.param'; +import { DeleteCardMessageParams } from './delete-card.message.param'; +import { DeleteContentElementMessageParams } from './delete-content-element.message.param'; import { DeleteColumnMessageParams } from './delete-column.message.param'; +import { FetchBoardMessageParams } from './fetch-board.message.param'; +import { FetchCardsMessageParams } from './fetch-cards.message.param'; +import { MoveCardMessageParams } from './move-card.message.param'; +import { MoveColumnMessageParams } from './move-column.message.param'; +import { MoveContentElementMessageParams } from './move-content-element.message.param'; +import { UpdateBoardTitleMessageParams } from './update-board-title.message.param'; +import { UpdateBoardVisibilityMessageParams } from './update-board-visibility.message.param'; +import { UpdateCardHeightMessageParams } from './update-card-height.message.param'; +import { UpdateCardTitleMessageParams } from './update-card-title.message.param'; +import { UpdateColumnTitleMessageParams } from './update-column-title.message.param'; +import { UpdateContentElementMessageParams } from './update-content-element.message.param'; -export { MoveCardMessageParams, CreateCardMessageParams, UpdateColumnTitleMessageParams, DeleteColumnMessageParams }; +export { + CreateCardMessageParams, + CreateColumnMessageParams, + CreateContentElementMessageParams, + DeleteBoardMessageParams, + DeleteCardMessageParams, + DeleteColumnMessageParams, + DeleteContentElementMessageParams, + FetchBoardMessageParams, + FetchCardsMessageParams, + MoveCardMessageParams, + MoveColumnMessageParams, + MoveContentElementMessageParams, + UpdateBoardTitleMessageParams, + UpdateBoardVisibilityMessageParams, + UpdateCardHeightMessageParams, + UpdateCardTitleMessageParams, + UpdateColumnTitleMessageParams, + UpdateContentElementMessageParams, +}; diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 4490d54f206..a0425ffa600 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -65,22 +65,24 @@ export class BoardUc { return board.context; } - async deleteBoard(userId: EntityId, boardId: EntityId): Promise { + async deleteBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'deleteBoard', userId, boardId }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); await this.boardPermissionService.checkPermission(userId, board, Action.write); await this.boardNodeService.delete(board); + return board; } - async updateBoardTitle(userId: EntityId, boardId: EntityId, title: string): Promise { + async updateBoardTitle(userId: EntityId, boardId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateBoardTitle', userId, boardId, title }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); await this.boardPermissionService.checkPermission(userId, board, Action.write); await this.boardNodeService.updateTitle(board, title); + return board; } async createColumn(userId: EntityId, boardId: EntityId): Promise { @@ -101,7 +103,7 @@ export class BoardUc { columnId: EntityId, targetBoardId: EntityId, targetPosition: number - ): Promise { + ): Promise { this.logger.debug({ action: 'moveColumn', userId, columnId, targetBoardId, targetPosition }); const column = await this.boardNodeService.findByClassAndId(Column, columnId); @@ -111,6 +113,7 @@ export class BoardUc { await this.boardPermissionService.checkPermission(userId, targetBoard, Action.write); await this.boardNodeService.move(column, targetBoard, targetPosition); + return column; } async copyBoard(userId: EntityId, boardId: EntityId): Promise { @@ -137,10 +140,11 @@ export class BoardUc { return copyStatus; } - async updateVisibility(userId: EntityId, boardId: EntityId, isVisible: boolean): Promise { + async updateVisibility(userId: EntityId, boardId: EntityId, isVisible: boolean): Promise { const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); await this.boardPermissionService.checkPermission(userId, board, Action.write); await this.boardNodeService.updateVisibility(board, isVisible); + return board; } } diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index 5d232081d92..19a37861ea4 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -143,6 +143,7 @@ describe(CardUc.name, () => { describe('when deleting a card', () => { it('should call the service to find the card', async () => { const { user, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.deleteCard(user.id, card.id); diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index eac9f9c00a3..9c627d0aa99 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -46,31 +46,35 @@ export class CardUc { return allowedCards; } - async updateCardHeight(userId: EntityId, cardId: EntityId, height: number): Promise { + async updateCardHeight(userId: EntityId, cardId: EntityId, height: number): Promise { this.logger.debug({ action: 'updateCardHeight', userId, cardId, height }); const card = await this.boardNodeService.findByClassAndId(Card, cardId); await this.boardNodePermissionService.checkPermission(userId, card, Action.write); await this.boardNodeService.updateHeight(card, height); + return card; } - async updateCardTitle(userId: EntityId, cardId: EntityId, title: string): Promise { + async updateCardTitle(userId: EntityId, cardId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateCardTitle', userId, cardId, title }); const card = await this.boardNodeService.findByClassAndId(Card, cardId); await this.boardNodePermissionService.checkPermission(userId, card, Action.write); await this.boardNodeService.updateTitle(card, title); + return card; } - async deleteCard(userId: EntityId, cardId: EntityId): Promise { + async deleteCard(userId: EntityId, cardId: EntityId): Promise { this.logger.debug({ action: 'deleteCard', userId, cardId }); const card = await this.boardNodeService.findByClassAndId(Card, cardId); + const { rootId } = card; await this.boardNodePermissionService.checkPermission(userId, card, Action.write); await this.boardNodeService.delete(card); + return rootId; } // --- elements --- @@ -98,7 +102,7 @@ export class CardUc { elementId: EntityId, targetCardId: EntityId, targetPosition: number - ): Promise { + ): Promise { this.logger.debug({ action: 'moveElement', userId, elementId, targetCardId, targetPosition }); const element = await this.boardNodeService.findContentElementById(elementId); @@ -108,5 +112,7 @@ export class CardUc { await this.boardNodePermissionService.checkPermission(userId, targetCard, Action.write); await this.boardNodeService.move(element, targetCard, targetPosition); + + return element; } } diff --git a/apps/server/src/modules/board/uc/column.uc.spec.ts b/apps/server/src/modules/board/uc/column.uc.spec.ts index 1c39a2fa57e..3c80734550e 100644 --- a/apps/server/src/modules/board/uc/column.uc.spec.ts +++ b/apps/server/src/modules/board/uc/column.uc.spec.ts @@ -73,6 +73,7 @@ describe(ColumnUc.name, () => { describe('when deleting a column', () => { it('should call the service to find the column', async () => { const { user, column } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); await uc.deleteColumn(user.id, column.id); diff --git a/apps/server/src/modules/board/uc/column.uc.ts b/apps/server/src/modules/board/uc/column.uc.ts index 8e8a0c1a06e..3f95c69cdbd 100644 --- a/apps/server/src/modules/board/uc/column.uc.ts +++ b/apps/server/src/modules/board/uc/column.uc.ts @@ -17,22 +17,26 @@ export class ColumnUc { this.logger.setContext(ColumnUc.name); } - async deleteColumn(userId: EntityId, columnId: EntityId): Promise { + async deleteColumn(userId: EntityId, columnId: EntityId): Promise { this.logger.debug({ action: 'deleteColumn', userId, columnId }); const column = await this.boardNodeService.findByClassAndId(Column, columnId); + const { rootId } = column; await this.boardNodePermissionService.checkPermission(userId, column, Action.write); await this.boardNodeService.delete(column); + + return rootId; } - async updateColumnTitle(userId: EntityId, columnId: EntityId, title: string): Promise { + async updateColumnTitle(userId: EntityId, columnId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateColumnTitle', userId, columnId, title }); const column = await this.boardNodeService.findByClassAndId(Column, columnId); await this.boardNodePermissionService.checkPermission(userId, column, Action.write); await this.boardNodeService.updateTitle(column, title); + return column; } async createCard( @@ -53,7 +57,7 @@ export class ColumnUc { return card; } - async moveCard(userId: EntityId, cardId: EntityId, targetColumnId: EntityId, targetPosition: number): Promise { + async moveCard(userId: EntityId, cardId: EntityId, targetColumnId: EntityId, targetPosition: number): Promise { this.logger.debug({ action: 'moveCard', userId, cardId, targetColumnId, toPosition: targetPosition }); const card = await this.boardNodeService.findByClassAndId(Card, cardId); @@ -63,5 +67,6 @@ export class ColumnUc { await this.boardNodePermissionService.checkPermission(userId, targetColumn, Action.write); await this.boardNodeService.move(card, targetColumn, targetPosition); + return card; } } diff --git a/apps/server/src/modules/board/uc/element.uc.spec.ts b/apps/server/src/modules/board/uc/element.uc.spec.ts index 2315b3788d0..471be5363a4 100644 --- a/apps/server/src/modules/board/uc/element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/element.uc.spec.ts @@ -135,6 +135,7 @@ describe(ElementUc.name, () => { it('should call the service to find the element', async () => { const { user, element } = setup(); + boardNodeService.findContentElementById.mockResolvedValueOnce(element); await uc.deleteElement(user.id, element.id); diff --git a/apps/server/src/modules/board/uc/element.uc.ts b/apps/server/src/modules/board/uc/element.uc.ts index cff947cd92a..0d07c9c468c 100644 --- a/apps/server/src/modules/board/uc/element.uc.ts +++ b/apps/server/src/modules/board/uc/element.uc.ts @@ -37,11 +37,13 @@ export class ElementUc { return element; } - async deleteElement(userId: EntityId, elementId: EntityId): Promise { + async deleteElement(userId: EntityId, elementId: EntityId): Promise { const element = await this.boardNodeService.findContentElementById(elementId); + const { rootId } = element; await this.boardPermissionService.checkPermission(userId, element, Action.write); await this.boardNodeService.delete(element); + return rootId; } async checkElementReadPermission(userId: EntityId, elementId: EntityId): Promise { From 38e166238d6509248d67ebf26c99ffa2c1f564bb Mon Sep 17 00:00:00 2001 From: Cedric Evers <12080057+CeEv@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:48:56 +0200 Subject: [PATCH 14/35] BC-7235 Add initial PerformanceObserver and use it for tldraw. (#5069) --- .../src/core/logger/types/logging.types.ts | 3 + apps/server/src/modules/tldraw/config.ts | 4 +- .../src/modules/tldraw/repo/y-mongodb.spec.ts | 12 +- .../src/modules/tldraw/repo/y-mongodb.ts | 81 ++++++++----- .../tldraw/service/tldraw.ws.service.spec.ts | 34 +++--- .../tldraw/service/tldraw.ws.service.ts | 108 ++++++++---------- .../modules/tldraw/tldraw-console.module.ts | 19 ++- .../src/modules/tldraw/tldraw-ws.module.ts | 26 +++-- apps/server/src/shared/common/error/index.ts | 1 + .../src/shared/common/error/interfaces.ts | 12 ++ .../src/shared/common/loggable/index.ts | 1 + .../src/shared/common/loggable/interfaces.ts | 18 +++ .../referenced-entity-not-found-loggable.ts | 4 +- .../src/shared/common/measure-utils/index.ts | 1 + .../performance-observer.spec.ts | 103 +++++++++++++++++ .../measure-utils/performance-observer.ts | 56 +++++++++ config/default.schema.json | 11 ++ 17 files changed, 362 insertions(+), 132 deletions(-) create mode 100644 apps/server/src/shared/common/error/interfaces.ts create mode 100644 apps/server/src/shared/common/loggable/interfaces.ts create mode 100644 apps/server/src/shared/common/measure-utils/index.ts create mode 100644 apps/server/src/shared/common/measure-utils/performance-observer.spec.ts create mode 100644 apps/server/src/shared/common/measure-utils/performance-observer.ts diff --git a/apps/server/src/core/logger/types/logging.types.ts b/apps/server/src/core/logger/types/logging.types.ts index c5fcdc1f8c9..3bd1839fd8b 100644 --- a/apps/server/src/core/logger/types/logging.types.ts +++ b/apps/server/src/core/logger/types/logging.types.ts @@ -1,3 +1,6 @@ +/** + * Information inside this file should be placed in shared, type are copied to it. + */ export type LogMessage = { message: string; data?: LogMessageData; diff --git a/apps/server/src/modules/tldraw/config.ts b/apps/server/src/modules/tldraw/config.ts index b0a017dd59c..534ecbc06aa 100644 --- a/apps/server/src/modules/tldraw/config.ts +++ b/apps/server/src/modules/tldraw/config.ts @@ -17,6 +17,7 @@ export interface TldrawConfig { API_HOST: number; TLDRAW_MAX_DOCUMENT_SIZE: number; TLDRAW_FINALIZE_DELAY: number; + PERFORMANCE_MEASURE_ENABLED: boolean; } export const TLDRAW_DB_URL: string = Configuration.get('TLDRAW_DB_URL') as string; @@ -24,7 +25,7 @@ export const TLDRAW_SOCKET_PORT = Configuration.get('TLDRAW__SOCKET_PORT') as nu const tldrawConfig = { TLDRAW_DB_URL, - NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, + NEST_LOG_LEVEL: Configuration.get('TLDRAW__LOG_LEVEL') as string, INCOMING_REQUEST_TIMEOUT: Configuration.get('INCOMING_REQUEST_TIMEOUT_API') as number, TLDRAW_DB_COMPRESS_THRESHOLD: Configuration.get('TLDRAW__DB_COMPRESS_THRESHOLD') as number, FEATURE_TLDRAW_ENABLED: Configuration.get('FEATURE_TLDRAW_ENABLED') as boolean, @@ -39,6 +40,7 @@ const tldrawConfig = { API_HOST: Configuration.get('API_HOST') as string, TLDRAW_MAX_DOCUMENT_SIZE: Configuration.get('TLDRAW__MAX_DOCUMENT_SIZE') as number, TLDRAW_FINALIZE_DELAY: Configuration.get('TLDRAW__FINALIZE_DELAY') as number, + PERFORMANCE_MEASURE_ENABLED: Configuration.get('TLDRAW__PERFORMANCE_MEASURE_ENABLED') as boolean, }; export const config = () => tldrawConfig; diff --git a/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts b/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts index a3a2ae88677..dbdd475a32f 100644 --- a/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts +++ b/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts @@ -1,17 +1,17 @@ +import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/mongodb'; +import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { cleanupCollections } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { ConfigModule } from '@nestjs/config'; -import { createMock } from '@golevelup/ts-jest'; -import * as Yjs from 'yjs'; import { createConfigModuleOptions } from '@src/config'; import { DomainErrorHandler } from '@src/core'; -import { tldrawEntityFactory, tldrawTestConfig } from '../testing'; +import * as Yjs from 'yjs'; import { TldrawDrawing } from '../entities'; +import { tldrawEntityFactory, tldrawTestConfig } from '../testing'; +import { Version } from './key.factory'; import { TldrawRepo } from './tldraw.repo'; import { YMongodb } from './y-mongodb'; -import { Version } from './key.factory'; jest.mock('yjs', () => { const moduleMock: unknown = { diff --git a/apps/server/src/modules/tldraw/repo/y-mongodb.ts b/apps/server/src/modules/tldraw/repo/y-mongodb.ts index 1ff357bba1c..edc7fae12fc 100644 --- a/apps/server/src/modules/tldraw/repo/y-mongodb.ts +++ b/apps/server/src/modules/tldraw/repo/y-mongodb.ts @@ -1,12 +1,12 @@ import { BulkWriteResult } from '@mikro-orm/mongodb/node_modules/mongodb'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { DomainErrorHandler } from '@src/core'; import { Buffer } from 'buffer'; import * as binary from 'lib0/binary'; import * as encoding from 'lib0/encoding'; import * as promise from 'lib0/promise'; import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector, mergeUpdates } from 'yjs'; -import { DomainErrorHandler } from '@src/core'; import { TldrawConfig } from '../config'; import { WsSharedDocDo } from '../domain'; import { TldrawDrawing } from '../entities'; @@ -94,7 +94,8 @@ export class YMongodb { // return value is not void, need to be changed public compressDocumentTransactional(docName: string): Promise { - // return value can be null, need to be defined + performance.mark('compressDocumentTransactional'); + return this._transact(docName, async () => { const updates = await this.getMongoUpdates(docName); const mergedUpdates = mergeUpdates(updates); @@ -105,10 +106,16 @@ export class YMongodb { const stateAsUpdate = encodeStateAsUpdate(ydoc); const sv = encodeStateVector(ydoc); const clock = await this.storeUpdate(docName, stateAsUpdate); + await this.writeStateVector(docName, sv, clock); await this.clearUpdatesRange(docName, 0, clock); ydoc.destroy(); + + performance.measure('tldraw:YMongodb:compressDocumentTransactional', { + start: 'compressDocumentTransactional', + detail: { doc_name: docName, clock }, + }); }); } @@ -143,20 +150,24 @@ export class YMongodb { return this.repo.readAsCursor(query, opts); } - private mergeDocsTogether(doc: TldrawDrawing, docs: TldrawDrawing[], docIndex: number): Buffer[] { - const parts = [Buffer.from(doc.value.buffer)]; - let currentPartId: number | undefined = doc.part; - for (let i = docIndex + 1; i < docs.length; i += 1) { - const part = docs[i]; - - if (!this.isSameClock(part, doc)) { + private mergeDocsTogether( + tldrawDrawingEntity: TldrawDrawing, + tldrawDrawingEntities: TldrawDrawing[], + docIndex: number + ): Buffer[] { + const parts = [Buffer.from(tldrawDrawingEntity.value.buffer)]; + let currentPartId: number | undefined = tldrawDrawingEntity.part; + for (let i = docIndex + 1; i < tldrawDrawingEntities.length; i += 1) { + const entity = tldrawDrawingEntities[i]; + + if (!this.isSameClock(entity, tldrawDrawingEntity)) { break; } - this.checkIfPartIsNextPartAfterCurrent(part, currentPartId); + this.checkIfPartIsNextPartAfterCurrent(entity, currentPartId); - parts.push(Buffer.from(part.value.buffer)); - currentPartId = part.part; + parts.push(Buffer.from(entity.value.buffer)); + currentPartId = entity.part; } return parts; @@ -165,20 +176,20 @@ export class YMongodb { /** * Convert the mongo document array to an array of values (as buffers) */ - private convertMongoUpdates(docs: TldrawDrawing[]): Buffer[] { - if (!Array.isArray(docs) || !docs.length) return []; + private convertMongoUpdates(tldrawDrawingEntities: TldrawDrawing[]): Buffer[] { + if (!Array.isArray(tldrawDrawingEntities) || !tldrawDrawingEntities.length) return []; const updates: Buffer[] = []; - for (let i = 0; i < docs.length; i += 1) { - const doc = docs[i]; + for (let i = 0; i < tldrawDrawingEntities.length; i += 1) { + const tldrawDrawingEntity = tldrawDrawingEntities[i]; - if (!doc.part) { - updates.push(Buffer.from(doc.value.buffer)); + if (!tldrawDrawingEntity.part) { + updates.push(Buffer.from(tldrawDrawingEntity.value.buffer)); } - if (doc.part === 1) { + if (tldrawDrawingEntity.part === 1) { // merge the docs together that got split because of mongodb size limits - const parts = this.mergeDocsTogether(doc, docs, i); + const parts = this.mergeDocsTogether(tldrawDrawingEntity, tldrawDrawingEntities, i); updates.push(Buffer.concat(parts)); } } @@ -189,10 +200,19 @@ export class YMongodb { * Get all document updates for a specific document. */ private async getMongoUpdates(docName: string, opts = {}): Promise { + performance.mark('getMongoUpdates'); + const uniqueKey = KeyFactory.createForUpdate(docName); - const docs = await this.getMongoBulkData(uniqueKey, opts); + const tldrawDrawingEntities = await this.getMongoBulkData(uniqueKey, opts); + + const buffer = this.convertMongoUpdates(tldrawDrawingEntities); + + performance.measure('tldraw:YMongodb:getMongoUpdates', { + start: 'getMongoUpdates', + detail: { doc_name: docName, loaded_tldraw_entities_total: tldrawDrawingEntities.length }, + }); - return this.convertMongoUpdates(docs); + return buffer; } private async writeStateVector(docName: string, sv: Uint8Array, clock: number): Promise { @@ -247,20 +267,23 @@ export class YMongodb { return clock + 1; } - private isSameClock(doc1: TldrawDrawing, doc2: TldrawDrawing): boolean { - return doc1.clock === doc2.clock; + private isSameClock(tldrawDrawingEntity1: TldrawDrawing, tldrawDrawingEntity2: TldrawDrawing): boolean { + return tldrawDrawingEntity1.clock === tldrawDrawingEntity2.clock; } - private checkIfPartIsNextPartAfterCurrent(part: TldrawDrawing, currentPartId: number | undefined): void { - if (part.part === undefined || currentPartId !== part.part - 1) { + private checkIfPartIsNextPartAfterCurrent( + tldrawDrawingEntity: TldrawDrawing, + currentPartId: number | undefined + ): void { + if (tldrawDrawingEntity.part === undefined || currentPartId !== tldrawDrawingEntity.part - 1) { throw new Error('Could not merge updates together because a part is missing'); } } - private extractClock(updates: TldrawDrawing[]): number { - if (updates.length === 0 || updates[0].clock == null) { + private extractClock(tldrawDrawingEntities: TldrawDrawing[]): number { + if (tldrawDrawingEntities.length === 0 || tldrawDrawingEntities[0].clock == null) { return -1; } - return updates[0].clock; + return tldrawDrawingEntities[0].clock; } } diff --git a/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts b/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts index 50b19dca282..61853806003 100644 --- a/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts +++ b/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts @@ -1,29 +1,29 @@ -import { Test } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { HttpService } from '@nestjs/axios'; import { INestApplication } from '@nestjs/common'; -import WebSocket from 'ws'; +import { ConfigModule } from '@nestjs/config'; import { WsAdapter } from '@nestjs/platform-ws'; -import { TextEncoder } from 'util'; -import * as Yjs from 'yjs'; -import * as SyncProtocols from 'y-protocols/sync'; -import * as AwarenessProtocol from 'y-protocols/awareness'; -import * as Ioredis from 'ioredis'; -import { encoding } from 'lib0'; -import { TldrawWsFactory } from '@shared/testing/factory/tldraw.ws.factory'; -import { HttpService } from '@nestjs/axios'; +import { Test } from '@nestjs/testing'; import { WebSocketReadyStateEnum } from '@shared/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ConfigModule } from '@nestjs/config'; +import { TldrawWsFactory } from '@shared/testing/factory/tldraw.ws.factory'; import { createConfigModuleOptions } from '@src/config'; -import { MongoMemoryDatabaseModule } from '@infra/database'; import { DomainErrorHandler } from '@src/core'; -import { TldrawRedisFactory, TldrawRedisService } from '../redis'; +import * as Ioredis from 'ioredis'; +import { encoding } from 'lib0'; +import { TextEncoder } from 'util'; +import WebSocket from 'ws'; +import * as AwarenessProtocol from 'y-protocols/awareness'; +import * as SyncProtocols from 'y-protocols/sync'; +import * as Yjs from 'yjs'; +import { TldrawWsService } from '.'; import { TldrawWs } from '../controller'; +import { WsSharedDocDo } from '../domain'; import { TldrawDrawing } from '../entities'; +import { MetricsService } from '../metrics'; +import { TldrawRedisFactory, TldrawRedisService } from '../redis'; import { TldrawBoardRepo, TldrawRepo, YMongodb } from '../repo'; import { TestConnection, tldrawTestConfig } from '../testing'; -import { WsSharedDocDo } from '../domain'; -import { MetricsService } from '../metrics'; -import { TldrawWsService } from '.'; jest.mock('yjs', () => { const moduleMock: unknown = { diff --git a/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts b/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts index 82deaf6ac3c..1fadacf0bfd 100644 --- a/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts +++ b/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts @@ -1,31 +1,23 @@ import { Injectable, NotAcceptableException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { DomainErrorHandler } from '@src/core'; +import { decoding, encoding } from 'lib0'; +import { Buffer } from 'node:buffer'; import WebSocket from 'ws'; import { encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'; -import { decoding, encoding } from 'lib0'; import { readSyncMessage, writeSyncStep1, writeSyncStep2, writeUpdate } from 'y-protocols/sync'; -import { Buffer } from 'node:buffer'; -import { YMap } from 'yjs/dist/src/types/YMap'; -import { DomainErrorHandler } from '@src/core'; -import { TldrawRedisService } from '../redis'; +import { TldrawConfig } from '../config'; +import { WsSharedDocDo } from '../domain'; import { CloseConnectionLoggable, WebsocketErrorLoggable, WebsocketMessageErrorLoggable, WsSharedDocErrorLoggable, } from '../loggable'; -import { TldrawConfig } from '../config'; -import { - AwarenessConnectionsUpdate, - TldrawAsset, - TldrawShape, - UpdateOrigin, - UpdateType, - WSMessageType, -} from '../types'; -import { WsSharedDocDo } from '../domain'; -import { TldrawBoardRepo } from '../repo'; import { MetricsService } from '../metrics'; +import { TldrawRedisService } from '../redis'; +import { TldrawBoardRepo } from '../repo'; +import { AwarenessConnectionsUpdate, UpdateOrigin, UpdateType, WSMessageType } from '../types'; @Injectable() export class TldrawWsService { @@ -42,6 +34,8 @@ export class TldrawWsService { } public async closeConnection(doc: WsSharedDocDo, ws: WebSocket): Promise { + performance.mark('closeConnection'); + if (doc.connections.has(ws)) { const controlledIds = doc.connections.get(ws); doc.connections.delete(ws); @@ -52,6 +46,11 @@ export class TldrawWsService { ws.close(); await this.finalizeIfNoConnections(doc); + + performance.measure('tldraw:TldrawWsService:closeConnection', { + start: 'closeConnection', + detail: { doc_name: doc.name, doc_connection_total: doc.connections.size }, + }); } public send(doc: WsSharedDocDo, ws: WebSocket, message: Uint8Array): void { @@ -179,6 +178,8 @@ export class TldrawWsService { }; public async setupWsConnection(ws: WebSocket, docName: string): Promise { + performance.mark('setupWsConnection'); + ws.binaryType = 'arraybuffer'; // get doc, initialize if it does not exist yet - update this.getDocument(docName) can be return null @@ -224,27 +225,36 @@ export class TldrawWsService { pongReceived = true; }); - { - // send initial doc state to client as update - this.sendInitialState(ws, doc); - - const syncEncoder = encoding.createEncoder(); - encoding.writeVarUint(syncEncoder, WSMessageType.SYNC); - writeSyncStep1(syncEncoder, doc); - this.send(doc, ws, encoding.toUint8Array(syncEncoder)); - - const awarenessStates = doc.awareness.getStates(); - if (awarenessStates.size > 0) { - const awarenessEncoder = encoding.createEncoder(); - encoding.writeVarUint(awarenessEncoder, WSMessageType.AWARENESS); - encoding.writeVarUint8Array( - awarenessEncoder, - encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())) - ); - this.send(doc, ws, encoding.toUint8Array(awarenessEncoder)); - } + // send initial doc state to client as update + this.sendInitialState(ws, doc); + + const syncEncoder = encoding.createEncoder(); + encoding.writeVarUint(syncEncoder, WSMessageType.SYNC); + writeSyncStep1(syncEncoder, doc); + this.send(doc, ws, encoding.toUint8Array(syncEncoder)); + + const awarenessStates = doc.awareness.getStates(); + if (awarenessStates.size > 0) { + const awarenessEncoder = encoding.createEncoder(); + encoding.writeVarUint(awarenessEncoder, WSMessageType.AWARENESS); + encoding.writeVarUint8Array( + awarenessEncoder, + encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())) + ); + this.send(doc, ws, encoding.toUint8Array(awarenessEncoder)); } + this.metricsService.incrementNumberOfUsersOnServerCounter(); + + performance.measure('tldraw:TldrawWsService:setupWsConnection', { + start: 'setupWsConnection', + detail: { + doc_name: doc.name, + doc_awareness_state_total: awarenessStates.size, + doc_connection_total: doc.connections.size, + pod_docs_total: this.docs.size, + }, + }); } private async finalizeIfNoConnections(doc: WsSharedDocDo) { @@ -274,34 +284,6 @@ export class TldrawWsService { } } - private syncDocumentAssetsWithShapes(doc: WsSharedDocDo): TldrawAsset[] { - // clean up assets that are not used as shapes anymore - // which can happen when users do undo/redo operations on assets - const assets: YMap = doc.getMap('assets'); - const shapes: YMap = doc.getMap('shapes'); - const usedShapesAsAssets: TldrawShape[] = []; - const usedAssets: TldrawAsset[] = []; - - for (const [, shape] of shapes) { - if (shape.assetId) { - usedShapesAsAssets.push(shape); - } - } - - doc.transact(() => { - for (const [, asset] of assets) { - const foundAsset = usedShapesAsAssets.some((shape) => shape.assetId === asset.id); - if (!foundAsset) { - assets.delete(asset.id); - } else { - usedAssets.push(asset); - } - } - }); - - return usedAssets; - } - private sendUpdateToConnectedClients(update: Uint8Array, doc: WsSharedDocDo): void { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, WSMessageType.SYNC); diff --git a/apps/server/src/modules/tldraw/tldraw-console.module.ts b/apps/server/src/modules/tldraw/tldraw-console.module.ts index fb3260def02..80cddece592 100644 --- a/apps/server/src/modules/tldraw/tldraw-console.module.ts +++ b/apps/server/src/modules/tldraw/tldraw-console.module.ts @@ -3,13 +3,14 @@ import { RabbitMQWrapperModule } from '@infra/rabbitmq'; 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 { DB_PASSWORD, DB_USERNAME, createConfigModuleOptions } from '@src/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { initialisePerformanceObserver } from '@shared/common/measure-utils'; +import { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; -import { LoggerModule } from '@src/core/logger'; +import { Logger, LoggerModule } from '@src/core/logger'; import { ConsoleModule } from 'nestjs-console'; import { FilesStorageClientModule } from '../files-storage-client'; -import { TLDRAW_DB_URL, config } from './config'; +import { config, TldrawConfig, TLDRAW_DB_URL } from './config'; import { TldrawDrawing } from './entities'; import { TldrawFilesConsole } from './job'; import { TldrawRepo, YMongodb } from './repo'; @@ -24,6 +25,7 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { @Module({ imports: [ + CoreModule, ConsoleModule, ConsoleWriterModule, RabbitMQWrapperModule, @@ -42,4 +44,11 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { ], providers: [TldrawRepo, YMongodb, TldrawFilesConsole, TldrawFilesStorageAdapterService, TldrawDeleteFilesUc], }) -export class TldrawConsoleModule {} +export class TldrawConsoleModule { + constructor(private readonly logger: Logger, private readonly configService: ConfigService) { + if (this.configService.get('PERFORMANCE_MEASURE_ENABLED') === true) { + this.logger.setContext('PerformanceObserver'); + initialisePerformanceObserver(this.logger); + } + } +} diff --git a/apps/server/src/modules/tldraw/tldraw-ws.module.ts b/apps/server/src/modules/tldraw/tldraw-ws.module.ts index 3f096352a4a..a4d7ba2ede6 100644 --- a/apps/server/src/modules/tldraw/tldraw-ws.module.ts +++ b/apps/server/src/modules/tldraw/tldraw-ws.module.ts @@ -1,18 +1,19 @@ +import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; +import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; +import { HttpModule } from '@nestjs/axios'; import { Module, NotFoundException } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { initialisePerformanceObserver } from '@shared/common/measure-utils'; import { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; -import { LoggerModule } from '@src/core/logger'; -import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; -import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; -import { HttpModule } from '@nestjs/axios'; +import { Logger, LoggerModule } from '@src/core/logger'; +import { config, TldrawConfig, TLDRAW_DB_URL } from './config'; +import { TldrawWs } from './controller'; import { TldrawDrawing } from './entities'; import { MetricsService } from './metrics'; +import { TldrawRedisFactory, TldrawRedisService } from './redis'; import { TldrawBoardRepo, TldrawRepo, YMongodb } from './repo'; import { TldrawWsService } from './service'; -import { TldrawWs } from './controller'; -import { config, TLDRAW_DB_URL } from './config'; -import { TldrawRedisFactory, TldrawRedisService } from './redis'; const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { findOneOrFailHandler: (entityName: string, where: Dictionary | IPrimaryKey) => @@ -45,4 +46,11 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { TldrawRedisService, ], }) -export class TldrawWsModule {} +export class TldrawWsModule { + constructor(private readonly logger: Logger, private readonly configService: ConfigService) { + if (this.configService.get('PERFORMANCE_MEASURE_ENABLED') === true) { + this.logger.setContext('PerformanceObserver'); + initialisePerformanceObserver(this.logger); + } + } +} diff --git a/apps/server/src/shared/common/error/index.ts b/apps/server/src/shared/common/error/index.ts index d82dbc40fbb..58446694939 100644 --- a/apps/server/src/shared/common/error/index.ts +++ b/apps/server/src/shared/common/error/index.ts @@ -4,6 +4,7 @@ export * from './business.error'; export * from './entity-not-found.error'; export * from './forbidden-operation.error'; export * from './validation.error'; +export * from './interfaces'; // business errors export * from './user-already-assigned-to-import-user.business-error'; diff --git a/apps/server/src/shared/common/error/interfaces.ts b/apps/server/src/shared/common/error/interfaces.ts new file mode 100644 index 00000000000..e9f58d88279 --- /dev/null +++ b/apps/server/src/shared/common/error/interfaces.ts @@ -0,0 +1,12 @@ +export type ErrorLogMessage = { + error?: Error; + type: string; // TODO: use enum + stack?: string; + data?: { [key: string]: string | number | boolean | undefined }; +}; + +export type ValidationErrorLogMessage = { + validationErrors: string[]; + stack?: string; + type: string; // TODO: use enum +}; diff --git a/apps/server/src/shared/common/loggable/index.ts b/apps/server/src/shared/common/loggable/index.ts index 5f21a462625..41cf4b21ac5 100644 --- a/apps/server/src/shared/common/loggable/index.ts +++ b/apps/server/src/shared/common/loggable/index.ts @@ -1 +1,2 @@ export { ReferencedEntityNotFoundLoggable } from './referenced-entity-not-found-loggable'; +export { Loggable, LoggableMessage } from './interfaces'; diff --git a/apps/server/src/shared/common/loggable/interfaces.ts b/apps/server/src/shared/common/loggable/interfaces.ts new file mode 100644 index 00000000000..359b5f37ad9 --- /dev/null +++ b/apps/server/src/shared/common/loggable/interfaces.ts @@ -0,0 +1,18 @@ +import { ErrorLogMessage, ValidationErrorLogMessage } from '../error'; + +type LogMessageDataObject = { + [key: string]: LogMessageData; +}; + +type LogMessageData = LogMessageDataObject | string | number | boolean | undefined; + +type LogMessage = { + message: string; + data?: LogMessageData; +}; + +export type LoggableMessage = LogMessage | ErrorLogMessage | ValidationErrorLogMessage; + +export interface Loggable { + getLogMessage(): LoggableMessage; +} diff --git a/apps/server/src/shared/common/loggable/referenced-entity-not-found-loggable.ts b/apps/server/src/shared/common/loggable/referenced-entity-not-found-loggable.ts index 10031eeb67e..2a6e33c97ff 100644 --- a/apps/server/src/shared/common/loggable/referenced-entity-not-found-loggable.ts +++ b/apps/server/src/shared/common/loggable/referenced-entity-not-found-loggable.ts @@ -1,5 +1,5 @@ -import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; import { EntityId } from '../../domain/types'; +import { Loggable, LoggableMessage } from './interfaces'; export class ReferencedEntityNotFoundLoggable implements Loggable { constructor( @@ -9,7 +9,7 @@ export class ReferencedEntityNotFoundLoggable implements Loggable { private readonly referencedEntityId: EntityId ) {} - getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + getLogMessage(): LoggableMessage { return { message: 'The requested entity could not been found, but it is still referenced.', data: { diff --git a/apps/server/src/shared/common/measure-utils/index.ts b/apps/server/src/shared/common/measure-utils/index.ts new file mode 100644 index 00000000000..e217f54ad43 --- /dev/null +++ b/apps/server/src/shared/common/measure-utils/index.ts @@ -0,0 +1 @@ +export * from './performance-observer'; diff --git a/apps/server/src/shared/common/measure-utils/performance-observer.spec.ts b/apps/server/src/shared/common/measure-utils/performance-observer.spec.ts new file mode 100644 index 00000000000..668ffc7ff29 --- /dev/null +++ b/apps/server/src/shared/common/measure-utils/performance-observer.spec.ts @@ -0,0 +1,103 @@ +import { PerformanceEntry } from 'node:perf_hooks'; +import { + InitialisePerformanceObserverLoggable, + MeasuresLoggable, + initialisePerformanceObserver, + closePerformanceObserver, +} from './performance-observer'; + +async function wait(timeoutMS: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, timeoutMS); + }); +} + +async function waitForEventLoopEnd(): Promise { + return new Promise((resolve) => { + process.nextTick(() => { + resolve(); + }); + }); +} + +describe('PerformanceObserver', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('InitialisePerformanceObserverLoggable', () => { + describe('getLogMessage', () => { + it('should be log correct formated message', () => { + const loggable = new InitialisePerformanceObserverLoggable(); + + const log = loggable.getLogMessage(); + + expect(log).toEqual({ + message: 'Initialise PerformanceObserver...', + }); + }); + }); + }); + + describe('MeasuresLoggable', () => { + describe('getLogMessage', () => { + const setup = () => { + const performanceEntry = { name: 'a', duration: 1, detail: { x: 1 } } as PerformanceEntry; + + const loggable = new MeasuresLoggable([performanceEntry, performanceEntry]); + + return { loggable }; + }; + + it('should be log correct formated message', () => { + const { loggable } = setup(); + + const log = loggable.getLogMessage(); + + expect(log).toEqual({ + message: 'Measure results', + data: '[{ location: a, duration: 1, detail: { x: 1 } }, { location: a, duration: 1, detail: { x: 1 } }]', + }); + }); + }); + }); + + describe('initialisePerformanceObserver', () => { + const setup = () => { + const mockInfoLogger = { + info: () => {}, + }; + const infoLoggerSpy = jest.spyOn(mockInfoLogger, 'info'); + + return { infoLoggerSpy, mockInfoLogger }; + }; + + it('should be log by execution', () => { + const { infoLoggerSpy, mockInfoLogger } = setup(); + + initialisePerformanceObserver(mockInfoLogger); + + expect(infoLoggerSpy).toHaveBeenNthCalledWith(1, new InitialisePerformanceObserverLoggable()); + + closePerformanceObserver(); + }); + + it('should be log messure if it is executed', async () => { + const { infoLoggerSpy, mockInfoLogger } = setup(); + initialisePerformanceObserver(mockInfoLogger); + infoLoggerSpy.mockClear(); + + performance.mark('startMark'); + await wait(1); + performance.measure('myMeasure', { + start: 'startMark', + detail: { x: 1 }, + }); + + await waitForEventLoopEnd(); + expect(infoLoggerSpy).toHaveBeenNthCalledWith(1, new MeasuresLoggable([])); + + closePerformanceObserver(); + }); + }); +}); diff --git a/apps/server/src/shared/common/measure-utils/performance-observer.ts b/apps/server/src/shared/common/measure-utils/performance-observer.ts new file mode 100644 index 00000000000..6b052b0fd3a --- /dev/null +++ b/apps/server/src/shared/common/measure-utils/performance-observer.ts @@ -0,0 +1,56 @@ +import { PerformanceEntry, PerformanceObserver } from 'node:perf_hooks'; +import util from 'util'; +import { Loggable, LoggableMessage } from '../loggable'; + +interface InfoLogger { + info(input: Loggable): void; +} + +export class MeasuresLoggable implements Loggable { + constructor(private readonly entries: PerformanceEntry[]) {} + + getLogMessage(): LoggableMessage { + const stringifiedEntries = this.entries.map((entry) => { + const detail = util.inspect(entry.detail).replace(/\n/g, '').replace(/\\n/g, ''); + return `{ location: ${entry.name}, duration: ${entry.duration}, detail: ${detail} }`; + }); + const data = `[${stringifiedEntries.join(', ')}]`; + const message = { message: `Measure results`, data }; + + return message; + } +} + +export class InitialisePerformanceObserverLoggable implements Loggable { + getLogMessage(): LoggableMessage { + return { + message: 'Initialise PerformanceObserver...', + }; + } +} + +let observer: PerformanceObserver | null = null; + +export const initialisePerformanceObserver = (infoLogger: InfoLogger): PerformanceObserver => { + infoLogger.info(new InitialisePerformanceObserverLoggable()); + + if (observer === null) { + observer = new PerformanceObserver((perfObserverEntryList) => { + const entries = perfObserverEntryList.getEntriesByType('measure'); + infoLogger.info(new MeasuresLoggable(entries)); + }); + + observer.observe({ type: 'measure', buffered: true }); + } + + return observer; +}; + +export const closePerformanceObserver = (): void => { + if (observer !== null) { + performance.clearMarks(); + performance.clearMeasures(); + observer.disconnect(); + observer = null; + } +}; diff --git a/config/default.schema.json b/config/default.schema.json index 5622b162ee8..4e17feb039d 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1577,6 +1577,17 @@ "type": "string", "description": "List with allowed assets MIME types, comma separated, empty if all MIME types supported by tldraw should be allowed", "examples": ["image/gif,image/jpeg,video/webm"] + }, + "PERFORMANCE_MEASURE_ENABLED": { + "type": "boolean", + "description": "Activate the performance measure for observed areas.", + "default": true + }, + "LOG_LEVEL": { + "type": "string", + "default": "info", + "description": "Define log level for tldraw.", + "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] } }, "default": { From c4bb1fc804117ac3989a93ccd8c5e719fe2b1ddb Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Tue, 2 Jul 2024 10:53:01 +0200 Subject: [PATCH 15/35] BC-7390 - add schoolyears to adminapi created schools (#5089) --- .../school.administration.api.spec.ts | 7 +- .../schoolyear-no-years-left.loggable.ts | 15 +++ .../repo/schoolyear.repo.integration.spec.ts | 100 ++++++++++++++++++ .../legacy-school/repo/schoolyear.repo.ts | 14 +++ .../service/legacy-school.service.spec.ts | 13 ++- .../service/legacy-school.service.ts | 16 ++- .../service/school-year.service.spec.ts | 24 +++++ .../service/school-year.service.ts | 6 ++ .../src/modules/user/uc/admin-api-user.uc.ts | 1 + config/development.json | 5 +- 10 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 apps/server/src/modules/legacy-school/loggable/schoolyear-no-years-left.loggable.ts diff --git a/apps/server/src/modules/legacy-school/controller/api-test/school.administration.api.spec.ts b/apps/server/src/modules/legacy-school/controller/api-test/school.administration.api.spec.ts index ed3643eb9ae..45b209ae7dc 100644 --- a/apps/server/src/modules/legacy-school/controller/api-test/school.administration.api.spec.ts +++ b/apps/server/src/modules/legacy-school/controller/api-test/school.administration.api.spec.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity } from '@shared/domain/entity'; -import { TestApiClient, federalStateFactory } from '@shared/testing'; +import { TestApiClient, federalStateFactory, schoolYearFactory } from '@shared/testing'; import { AdminApiServerTestModule } from '@src/modules/server/admin-api.server.module'; import { AuthGuard } from '@nestjs/passport'; import { AdminApiSchoolCreateResponseDto } from '../dto/response/admin-api-school-create.response.dto'; @@ -55,8 +55,9 @@ describe('Admin API - Schools (API)', () => { describe('with api token', () => { const setup = async () => { const federalState = federalStateFactory.build({ name: 'niedersachsen' }); - await em.persistAndFlush(federalState); - return { federalState }; + const year = schoolYearFactory.build(); + await em.persistAndFlush([federalState, year]); + return { federalState, year }; }; it('should return school', async () => { diff --git a/apps/server/src/modules/legacy-school/loggable/schoolyear-no-years-left.loggable.ts b/apps/server/src/modules/legacy-school/loggable/schoolyear-no-years-left.loggable.ts new file mode 100644 index 00000000000..a9dc93fe0f0 --- /dev/null +++ b/apps/server/src/modules/legacy-school/loggable/schoolyear-no-years-left.loggable.ts @@ -0,0 +1,15 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class SchoolYearsNoYearsLeft extends InternalServerErrorException implements Loggable { + // this is a 500, because our development team is responsible to create schoolyears. + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + /* istanbul ignore next */ + return { + type: 'SCHOOL_YEARS_NO_YEARS_LEFT', + message: + 'Could not find any schoolyears with an enddate later than the current date. Check if the next schoolyear has been created in the database.', + stack: this.stack, + }; + } +} 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 241200e81c2..0d2f510f467 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 @@ -125,4 +125,104 @@ describe('schoolyear repo', () => { }); }); }); + + describe('findCurrentOrNextYear', () => { + describe('when current date is within a schoolyear', () => { + const setup = async () => { + jest + .useFakeTimers({ advanceTimers: true, doNotFake: ['setInterval', 'clearInterval', 'setTimeout'] }) + .setSystemTime(new Date('2021-01-01')); + + const previousYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2019-09-01'), + endDate: new Date('2020-07-31'), + }); + + const currentYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2020-09-01'), + endDate: new Date('2021-07-31'), + }); + + const nextYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2021-09-01'), + endDate: new Date('2022-07-31'), + }); + + await em.persistAndFlush([previousYear, currentYear, nextYear]); + em.clear(); + + return { previousYear, currentYear, nextYear }; + }; + + it('should return the current schoolyear', async () => { + const { currentYear } = await setup(); + + const result = await repo.findCurrentOrNextYear(); + + expect(result).toEqual(currentYear); + }); + }); + + describe('when current date is during a summerbreak', () => { + const setup = async () => { + jest + .useFakeTimers({ advanceTimers: true, doNotFake: ['setInterval', 'clearInterval', 'setTimeout'] }) + .setSystemTime(new Date('2020-08-31')); + + const previousYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2019-09-01'), + endDate: new Date('2020-07-31'), + }); + + const nextYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2020-09-01'), + endDate: new Date('2021-07-31'), + }); + + await em.persistAndFlush([previousYear, nextYear]); + em.clear(); + + return { previousYear, nextYear }; + }; + + it('should return the upcoming schoolyear', async () => { + const { nextYear } = await setup(); + + const result = await repo.findCurrentOrNextYear(); + + expect(result).toEqual(nextYear); + }); + }); + + describe('when current date is later than any year', () => { + const setup = async () => { + jest + .useFakeTimers({ advanceTimers: true, doNotFake: ['setInterval', 'clearInterval', 'setTimeout'] }) + .setSystemTime(new Date('2030-08-31')); + + const previousYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2019-09-01'), + endDate: new Date('2020-07-31'), + }); + + const nextYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2020-09-01'), + endDate: new Date('2021-07-31'), + }); + + await em.persistAndFlush([previousYear, nextYear]); + em.clear(); + + return { previousYear, nextYear }; + }; + + it('should throw', async () => { + await setup(); + + const func = () => repo.findCurrentOrNextYear(); + + await expect(func).rejects.toThrow(); + }); + }); + }); }); diff --git a/apps/server/src/modules/legacy-school/repo/schoolyear.repo.ts b/apps/server/src/modules/legacy-school/repo/schoolyear.repo.ts index 14115f36002..635f929918f 100644 --- a/apps/server/src/modules/legacy-school/repo/schoolyear.repo.ts +++ b/apps/server/src/modules/legacy-school/repo/schoolyear.repo.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { SchoolYearEntity } from '@shared/domain/entity'; import { BaseRepo } from '@shared/repo/base.repo'; +import { SchoolYearsNoYearsLeft } from '../loggable/schoolyear-no-years-left.loggable'; @Injectable() export class SchoolYearRepo extends BaseRepo { @@ -15,4 +16,17 @@ export class SchoolYearRepo extends BaseRepo { }); return year; } + + async findCurrentOrNextYear(): Promise { + const currentDate = new Date(); + const years: SchoolYearEntity[] = await this._em.find( + SchoolYearEntity, + { endDate: { $gte: currentDate } }, + { orderBy: { endDate: 'ASC' } } + ); + if (years.length < 1) { + throw new SchoolYearsNoYearsLeft(); + } + return years[0]; + } } diff --git a/apps/server/src/modules/legacy-school/service/legacy-school.service.spec.ts b/apps/server/src/modules/legacy-school/service/legacy-school.service.spec.ts index c51a405eb52..67281d163cd 100644 --- a/apps/server/src/modules/legacy-school/service/legacy-school.service.spec.ts +++ b/apps/server/src/modules/legacy-school/service/legacy-school.service.spec.ts @@ -3,10 +3,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { SchoolFeature } from '@shared/domain/types'; import { LegacySchoolRepo } from '@shared/repo'; -import { federalStateFactory, legacySchoolDoFactory, setupEntities } from '@shared/testing'; +import { federalStateFactory, legacySchoolDoFactory, schoolYearFactory, setupEntities } from '@shared/testing'; import { FederalStateService } from './federal-state.service'; import { LegacySchoolService } from './legacy-school.service'; import { SchoolValidationService } from './validation/school-validation.service'; +import { SchoolYearService } from './school-year.service'; describe('LegacySchoolService', () => { let module: TestingModule; @@ -15,6 +16,7 @@ describe('LegacySchoolService', () => { let schoolRepo: DeepMocked; let schoolValidationService: DeepMocked; let federalStateService: DeepMocked; + let schoolYearService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -32,6 +34,10 @@ describe('LegacySchoolService', () => { provide: FederalStateService, useValue: createMock(), }, + { + provide: SchoolYearService, + useValue: createMock(), + }, ], }).compile(); @@ -39,6 +45,7 @@ describe('LegacySchoolService', () => { schoolService = module.get(LegacySchoolService); schoolValidationService = module.get(SchoolValidationService); federalStateService = module.get(FederalStateService); + schoolYearService = module.get(SchoolYearService); await setupEntities(); }); @@ -357,7 +364,9 @@ describe('LegacySchoolService', () => { const name = 'Hogwarts'; const federalStateName = 'maybescottland?'; const federalState = federalStateFactory.build({ name: federalStateName }); + const year = schoolYearFactory.build(); federalStateService.findFederalStateByName.mockResolvedValue(federalState); + schoolYearService.getCurrentOrNextSchoolYear.mockResolvedValue(year); const school = await schoolService.createSchool({ name, federalStateName }); expect(school.name).toEqual(name); @@ -368,7 +377,9 @@ describe('LegacySchoolService', () => { const name = 'Hogwarts'; const federalStateName = 'maybescottland?'; const federalState = federalStateFactory.build({ name: federalStateName }); + const year = schoolYearFactory.build(); federalStateService.findFederalStateByName.mockResolvedValue(federalState); + schoolYearService.getCurrentOrNextSchoolYear.mockResolvedValue(year); const school = await schoolService.createSchool({ name, federalStateName }); expect(schoolRepo.save).toHaveBeenCalledWith(school); diff --git a/apps/server/src/modules/legacy-school/service/legacy-school.service.ts b/apps/server/src/modules/legacy-school/service/legacy-school.service.ts index db39ba7159b..0f74e1feeaf 100644 --- a/apps/server/src/modules/legacy-school/service/legacy-school.service.ts +++ b/apps/server/src/modules/legacy-school/service/legacy-school.service.ts @@ -4,6 +4,7 @@ import { EntityId, SchoolFeature } from '@shared/domain/types'; import { LegacySchoolRepo } from '@shared/repo'; import { FederalStateService } from './federal-state.service'; import { SchoolValidationService } from './validation'; +import { SchoolYearService } from './school-year.service'; /** * @deprecated because it uses the deprecated LegacySchoolDo. @@ -13,7 +14,8 @@ export class LegacySchoolService { constructor( private readonly schoolRepo: LegacySchoolRepo, private readonly schoolValidationService: SchoolValidationService, - private readonly federalStateService: FederalStateService + private readonly federalStateService: FederalStateService, + private readonly schoolYearService: SchoolYearService ) {} async hasFeature(schoolId: EntityId, feature: SchoolFeature): Promise { @@ -59,7 +61,17 @@ export class LegacySchoolService { async createSchool(props: { name: string; federalStateName: string }): Promise { const federalState = await this.federalStateService.findFederalStateByName(props.federalStateName); - const school = new LegacySchoolDo({ name: props.name, federalState }); + const schoolYear = await this.schoolYearService.getCurrentOrNextSchoolYear(); + const defaults = { + // fileStorageType: 'awsS3', + schoolYear, + permissions: { + teacher: { + STUDENT_LIST: true, + }, + }, + }; + const school = new LegacySchoolDo({ ...defaults, name: props.name, federalState }); await this.schoolRepo.save(school); return school; } diff --git a/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts b/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts index 4a580159a79..e2ba8097c36 100644 --- a/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts +++ b/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts @@ -57,6 +57,30 @@ describe('SchoolYearService', () => { }); }); + describe('getCurrentOrNextSchoolYear', () => { + const setup = () => { + const schoolYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2021-09-01'), + endDate: new Date('2022-12-31'), + }); + schoolYearRepo.findCurrentOrNextYear.mockResolvedValue(schoolYear); + + return { + schoolYear, + }; + }; + + describe('when called', () => { + it('should return the current school year', async () => { + const { schoolYear } = setup(); + + const currentSchoolYear: SchoolYearEntity = await service.getCurrentOrNextSchoolYear(); + + expect(currentSchoolYear).toEqual(schoolYear); + }); + }); + }); + describe('findById', () => { const setup = () => { const schoolYear: SchoolYearEntity = schoolYearFactory.build({ diff --git a/apps/server/src/modules/legacy-school/service/school-year.service.ts b/apps/server/src/modules/legacy-school/service/school-year.service.ts index d9a70b7a841..e52e3c0dae9 100644 --- a/apps/server/src/modules/legacy-school/service/school-year.service.ts +++ b/apps/server/src/modules/legacy-school/service/school-year.service.ts @@ -14,6 +14,12 @@ export class SchoolYearService { return current; } + async getCurrentOrNextSchoolYear(): Promise { + const current: SchoolYearEntity = await this.schoolYearRepo.findCurrentOrNextYear(); + + return current; + } + async findById(id: EntityId): Promise { const year: SchoolYearEntity = await this.schoolYearRepo.findById(id); diff --git a/apps/server/src/modules/user/uc/admin-api-user.uc.ts b/apps/server/src/modules/user/uc/admin-api-user.uc.ts index adf4242a0f5..bf58d4e9493 100644 --- a/apps/server/src/modules/user/uc/admin-api-user.uc.ts +++ b/apps/server/src/modules/user/uc/admin-api-user.uc.ts @@ -34,6 +34,7 @@ export class AdminApiUserUc { username: props.email, userId: user.id, password: initialPassword, + activated: true, } as AccountSave); return { userId: user.id, diff --git a/config/development.json b/config/development.json index bf191822f66..006601dd3cf 100644 --- a/config/development.json +++ b/config/development.json @@ -92,5 +92,8 @@ "URI": "http://localhost:9001/api/1.2.14" }, "PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL": "http://localhost:8888/v1/lizenzinfo", - "BOARD_COLLABORATION_URI": "ws://localhost:4450" + "BOARD_COLLABORATION_URI": "ws://localhost:4450", + "ADMIN_API": { + "ALLOWED_API_KEYS": "thisisasupersecureapikeythatisabsolutelysave" + } } From 1b49240ef5ac924eb72e898b050cad7b78d0c35a Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Tue, 2 Jul 2024 11:20:32 +0200 Subject: [PATCH 16/35] N21-2063 removes iservs oauth config to hide login button for dev (#4942) --- .../templates/configmap_file_init.yml.j2 | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) 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 bb30700349e..ca00617cc7f 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 @@ -202,6 +202,21 @@ data: # the additional namespace intended for use for the testing (and development) purposes if one want # to test anything that includes signing in with the IServ on nbc instance, but don't want to use # the default dev nbc instance as it would require merging the code to the main branch first. + # Removed oauth config + # "oauthConfig": { + # "clientId": "'$ISERV_OAUTH_CLIENT_ID'", + # "clientSecret": "'$ISERV_OAUTH_CLIENT_SECRET'", + # "tokenEndpoint": "'$ISERV_URL'/iserv/auth/public/token", + # "grantType": "authorization_code", + # "scope": "openid uuid", + # "responseType": "code", + # "redirectUri": "https://'$NS'.nbc.dbildungscloud.dev/api/v3/sso/oauth", + # "authEndpoint": "'$ISERV_URL'/iserv/auth/auth", + # "provider": "iserv", + # "logoutEndpoint": "'$ISERV_URL'/iserv/auth/logout", + # "jwksEndpoint": "'$ISERV_URL'/iserv/public/jwk", + # "issuer": "'$ISERV_URL'" + # } if [ "$SC_THEME" = "n21" ] && [[ "$NS" =~ ^(main|iserv-test)$ ]]; then ISERV_SYSTEM_ID=0000d186816abba584714c92 @@ -235,20 +250,6 @@ data: }, "type": "ldap", "provisioningStrategy": "iserv", - "oauthConfig": { - "clientId": "'$ISERV_OAUTH_CLIENT_ID'", - "clientSecret": "'$ISERV_OAUTH_CLIENT_SECRET'", - "tokenEndpoint": "'$ISERV_URL'/iserv/auth/public/token", - "grantType": "authorization_code", - "scope": "openid uuid", - "responseType": "code", - "redirectUri": "https://'$NS'.nbc.dbildungscloud.dev/api/v3/sso/oauth", - "authEndpoint": "'$ISERV_URL'/iserv/auth/auth", - "provider": "iserv", - "logoutEndpoint": "'$ISERV_URL'/iserv/auth/logout", - "jwksEndpoint": "'$ISERV_URL'/iserv/public/jwk", - "issuer": "'$ISERV_URL'" - } }, { "upsert": true From 37631f49634f44f64f3363cacf522fbe03b02984 Mon Sep 17 00:00:00 2001 From: odalys-dataport <82401838+odalys-dataport@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:14:53 +0200 Subject: [PATCH 17/35] BC-6930 - enable new layout on prod (#5088) --- config/default.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/default.schema.json b/config/default.schema.json index 4e17feb039d..db0a429e965 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1700,7 +1700,7 @@ }, "FEATURE_NEW_LAYOUT_ENABLED": { "type": "boolean", - "default": false, + "default": true, "description": "Enables the new layout feature" } }, From 7874e4db4363eb392e88413acc349fc2d79b79df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:15:51 +0200 Subject: [PATCH 18/35] N21-1996 Remove dynamic values from school external tool persistence (#5084) --- .../mikro-orm/Migration20240627134214.ts | 38 ++ ...ulconnex-tool-provisioning.service.spec.ts | 9 +- .../schulconnex-tool-provisioning.service.ts | 1 + .../tool-configuration-status.service.spec.ts | 2 +- .../tool-configuration-status.service.ts | 2 +- .../external-tool-datasheet.mapper.spec.ts | 4 +- .../mapper/external-tool-datasheet.mapper.ts | 2 +- ...xternal-tool-configuration.service.spec.ts | 8 +- .../external-tool-configuration.service.ts | 2 +- .../service/external-tool.service.spec.ts | 12 +- .../api-test/tool-school.api.spec.ts | 29 +- .../controller/dto/index.ts | 2 +- ...ol-external-tool-configuration.response.ts | 6 +- .../dto/school-external-tool.response.ts | 10 +- .../controller/tool-school.controller.ts | 27 +- .../tool/school-external-tool/domain/index.ts | 1 + ...hool-external-tool-configuration-status.ts | 4 +- .../domain/school-external-tool.do.ts | 14 +- ...l-tool-configuration-status.entity.spec.ts | 38 -- ...ternal-tool-configuration-status.entity.ts | 15 - .../school-external-tool.entity.spec.ts | 103 ----- .../entity/school-external-tool.entity.ts | 11 +- .../tool/school-external-tool/mapper/index.ts | 1 - ...chool-external-tool-request.mapper.spec.ts | 19 +- .../school-external-tool-request.mapper.ts | 21 +- ...hool-external-tool-response.mapper.spec.ts | 116 ----- .../school-external-tool-response.mapper.ts | 29 +- ...ol-external-tool-status-response.mapper.ts | 14 - .../school-external-tool.service.spec.ts | 403 +++++++----------- .../service/school-external-tool.service.ts | 65 ++- .../school-external-tool/testing/index.ts | 1 - ...ool-configuration-status-entity.factory.ts | 10 - ...l-configuration-status-response.factory.ts | 2 +- ...ernal-tool-configuration-status.factory.ts | 4 +- .../school-external-tool-entity.factory.ts | 3 +- .../testing/school-external-tool.factory.ts | 1 + .../uc/dto/school-external-tool.types.ts | 4 - .../uc/school-external-tool.uc.ts | 8 +- .../src/modules/tool/tool-api.module.ts | 3 - .../tool-launch.controller.api.spec.ts | 9 +- .../school-external-tool.repo.spec.ts | 14 +- .../school-external-tool.repo.ts | 4 +- .../school-external-tool.scope.spec.ts | 7 +- .../school-external-tool.scope.ts | 8 +- backup/setup/migrations.json | 9 + 45 files changed, 356 insertions(+), 739 deletions(-) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240627134214.ts rename apps/server/src/modules/tool/school-external-tool/{controller => }/domain/school-external-tool-configuration-status.ts (70%) delete mode 100644 apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.spec.ts delete mode 100644 apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.ts delete mode 100644 apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts delete mode 100644 apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts delete mode 100644 apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-status-response.mapper.ts delete mode 100644 apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-entity.factory.ts diff --git a/apps/server/src/migrations/mikro-orm/Migration20240627134214.ts b/apps/server/src/migrations/mikro-orm/Migration20240627134214.ts new file mode 100644 index 00000000000..95372879c47 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240627134214.ts @@ -0,0 +1,38 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240627134214 extends Migration { + async up(): Promise { + await this.driver.aggregate('school-external-tools', [ + { $set: { isDeactivated: { $ifNull: ['$status.isDeactivated', false] } } }, + { $unset: 'status' }, + { $out: 'school-external-tools' }, + ]); + + console.info(`'status.isDeactivated' has moved to 'isDeactivated' for all school-external-tools`); + } + + async down(): Promise { + await this.driver.nativeUpdate( + 'school-external-tools', + { isDeactivated: true }, + { + $set: { + status: { + isOutdatedOnScopeSchool: false, + isDeactivated: true, + }, + }, + } + ); + + await this.driver.nativeUpdate( + 'school-external-tools', + {}, + { + $unset: { isDeactivated: '' }, + } + ); + + console.info(`All school-external-tools were reverted to using 'status'`); + } +} diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts index 9601b32ab9a..ed3a05aa0c1 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts @@ -147,14 +147,15 @@ describe(SchulconnexToolProvisioningService.name, () => { await service.provisionSchoolExternalTools(userId, schoolId, systemId); - expect(schoolExternalToolService.saveSchoolExternalTool).toHaveBeenCalledWith({ - props: { + expect(schoolExternalToolService.saveSchoolExternalTool).toHaveBeenCalledWith<[SchoolExternalTool]>( + new SchoolExternalTool({ id: expect.any(String), toolId: externalTool.id, + isDeactivated: false, schoolId, parameters: [], - }, - }); + }) + ); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts index 75505abe65f..c24eea36374 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts @@ -72,6 +72,7 @@ export class SchulconnexToolProvisioningService { id: new ObjectId().toHexString(), toolId: externalTool.id, schoolId, + isDeactivated: false, parameters: [], }); diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts index 1dc112369d3..587d9731b6f 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts @@ -443,7 +443,7 @@ describe(ToolConfigurationStatusService.name, () => { const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, - status: { isDeactivated: true }, + isDeactivated: true, }); const contextExternalTool = contextExternalToolFactory .withSchoolExternalToolRef(schoolExternalTool.id) diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts index b5464fbac89..e1387d59dc5 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts @@ -72,7 +72,7 @@ export class ToolConfigurationStatusService { } private isToolDeactivated(externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool): boolean { - return !!(externalTool.isDeactivated || (schoolExternalTool.status && schoolExternalTool.status.isDeactivated)); + return externalTool.isDeactivated || schoolExternalTool.isDeactivated; } private async isToolLicensed(externalTool: ExternalTool, userId: EntityId): Promise { diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.spec.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.spec.ts index 26bc472710c..3cc59845750 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.spec.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.spec.ts @@ -37,7 +37,7 @@ describe(ExternalToolDatasheetMapper.name, () => { restrictToContexts: [ToolContextType.COURSE, ToolContextType.BOARD_ELEMENT], }); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - status: { isDeactivated: true }, + isDeactivated: true, }); const expectDatasheet: ExternalToolDatasheetTemplateData = externalToolDatasheetTemplateDataFactory .withOptionalProperties() @@ -68,7 +68,7 @@ describe(ExternalToolDatasheetMapper.name, () => { const school: School = schoolFactory.build(); const externalTool = externalToolFactory.build(); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - status: { isDeactivated: true }, + isDeactivated: true, }); const expectDatasheet: ExternalToolDatasheetTemplateData = externalToolDatasheetTemplateDataFactory.build({ instance: 'dBildungscloud', diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.ts index 4a336ff604e..d35e86b8d82 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-datasheet.mapper.ts @@ -61,7 +61,7 @@ export class ExternalToolDatasheetMapper { return 'Das Tool ist instanzweit deaktiviert'; } - if (schoolExternalTool?.status?.isDeactivated) { + if (schoolExternalTool?.isDeactivated) { return 'Das Tool ist deaktiviert'; } diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts index 54b5149f57a..4afa83fe301 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts @@ -129,12 +129,12 @@ describe('ExternalToolConfigurationService', () => { availableSchoolExternalTools.forEach((tool): void => { if (tool.id === 'deactivatedToolId') { tool.status = schoolExternalToolConfigurationStatusFactory.build({ - isDeactivated: true, + isGloballyDeactivated: true, isOutdatedOnScopeSchool: false, }); } tool.status = schoolExternalToolConfigurationStatusFactory.build({ - isDeactivated: false, + isGloballyDeactivated: false, isOutdatedOnScopeSchool: false, }); }); @@ -175,9 +175,7 @@ describe('ExternalToolConfigurationService', () => { ); expect( - result.every( - (toolInfo: ContextExternalToolTemplateInfo) => !toolInfo.schoolExternalTool.status?.isDeactivated - ) + result.every((toolInfo: ContextExternalToolTemplateInfo) => !toolInfo.schoolExternalTool.isDeactivated) ).toBe(true); }); }); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts index e46ad499f8c..f41dcace98e 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts @@ -38,7 +38,7 @@ export class ExternalToolConfigurationService { const availableTools: ContextExternalToolTemplateInfo[] = unusedTools .filter((toolRef): toolRef is ContextExternalToolTemplateInfo => !toolRef.externalTool.isHidden) .filter((toolRef) => !toolRef.externalTool.isDeactivated) - .filter((toolRef) => !toolRef.schoolExternalTool.status?.isDeactivated); + .filter((toolRef) => !toolRef.schoolExternalTool.isDeactivated); return availableTools; } 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 b43a67a37b2..abc6ec6ab07 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 @@ -10,6 +10,7 @@ import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } fro import { LegacyLogger } from '@src/core/logger'; import { ExternalToolSearchQuery } from '../../common/interface'; import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { schoolExternalToolFactory } from '../../school-external-tool/testing'; import { ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '../domain'; import { externalToolFactory, lti11ToolConfigFactory, oauth2ToolConfigFactory } from '../testing'; import { ExternalToolServiceMapper } from './external-tool-service.mapper'; @@ -375,16 +376,13 @@ describe(ExternalToolService.name, () => { const setup = () => { createTools(); - const schoolExternalTool: SchoolExternalTool = new SchoolExternalTool({ - id: 'schoolTool1', - toolId: 'tool1', - schoolId: 'school1', - parameters: [], - }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); schoolToolRepo.findByExternalToolId.mockResolvedValue([schoolExternalTool]); - return { schoolExternalTool }; + return { + schoolExternalTool, + }; }; describe('when tool id is set', () => { 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 1b62a7128fc..b75027fbb60 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 @@ -130,8 +130,9 @@ describe('ToolSchoolController (API)', () => { const response = await loggedInClient.post().send(postParams); expect(response.statusCode).toEqual(HttpStatus.CREATED); - expect(response.body).toEqual({ + expect(response.body).toEqual({ id: expect.any(String), + isDeactivated: postParams.isDeactivated, name: externalToolEntity.name, schoolId: postParams.schoolId, toolId: postParams.toolId, @@ -231,6 +232,7 @@ describe('ToolSchoolController (API)', () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ parameters: [], + isDeactivated: true, }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ @@ -284,16 +286,18 @@ describe('ToolSchoolController (API)', () => { expect(response.statusCode).toEqual(HttpStatus.OK); expect(response.body).toEqual( - expect.objectContaining({ + expect.objectContaining({ data: [ { id: schoolExternalToolEntity.id, name: externalToolEntity.name, schoolId: school.id, toolId: externalToolEntity.id, - status: schoolExternalToolConfigurationStatusFactory.build({ + isDeactivated: false, + status: { isOutdatedOnScopeSchool: true, - }), + isGloballyDeactivated: true, + }, parameters: [ { name: schoolExternalToolEntity.schoolParameters[0].name, @@ -322,6 +326,7 @@ describe('ToolSchoolController (API)', () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ parameters: [], + isDeactivated: true, }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ @@ -331,12 +336,14 @@ describe('ToolSchoolController (API)', () => { const schoolExternalToolResponse: SchoolExternalToolResponse = new SchoolExternalToolResponse({ id: schoolExternalToolEntity.id, - name: '', + name: externalToolEntity.name, schoolId: school.id, toolId: externalToolEntity.id, - status: schoolExternalToolConfigurationStatusFactory.build({ - isOutdatedOnScopeSchool: false, - }), + isDeactivated: false, + status: { + isOutdatedOnScopeSchool: true, + isGloballyDeactivated: true, + }, parameters: [ { name: schoolExternalToolEntity.schoolParameters[0].name, @@ -459,9 +466,11 @@ describe('ToolSchoolController (API)', () => { name: externalToolEntity.name, schoolId: postParamsUpdate.schoolId, toolId: postParamsUpdate.toolId, - status: schoolExternalToolConfigurationStatusFactory.build({ + isDeactivated: false, + status: { isOutdatedOnScopeSchool: false, - }), + isGloballyDeactivated: false, + }, parameters: [ { name: updatedParamEntry.name, diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts index 3631b6989b4..b3e0ba2d6ed 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts @@ -6,4 +6,4 @@ export * from './school-external-tool-post.params'; export * from './school-external-tool-search.params'; export * from './school-external-tool-search-list.response'; export * from './school-external-tool-metadata.response'; -export * from '../domain/school-external-tool-configuration-status'; +export { SchoolExternalToolConfigurationStatusResponse } from './school-external-tool-configuration.response'; diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-configuration.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-configuration.response.ts index 36d500ba88e..2807b060c22 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-configuration.response.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-configuration.response.ts @@ -10,12 +10,12 @@ export class SchoolExternalToolConfigurationStatusResponse { @ApiProperty({ type: Boolean, - description: 'Is the tool deactivated, because of school administrator?', + description: 'Is the tool deactivated, because of instance administrator?', }) - isDeactivated: boolean; + isGloballyDeactivated: boolean; constructor(props: SchoolExternalToolConfigurationStatusResponse) { this.isOutdatedOnScopeSchool = props.isOutdatedOnScopeSchool; - this.isDeactivated = props.isDeactivated; + this.isGloballyDeactivated = props.isGloballyDeactivated; } } diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts index 2ca015c5538..76e4affd524 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { CustomParameterEntryResponse } from './custom-parameter-entry.response'; import { SchoolExternalToolConfigurationStatusResponse } from './school-external-tool-configuration.response'; @@ -15,22 +15,22 @@ export class SchoolExternalToolResponse { @ApiProperty() schoolId: string; + @ApiProperty() + isDeactivated: boolean; + @ApiProperty({ type: [CustomParameterEntryResponse] }) parameters: CustomParameterEntryResponse[]; @ApiProperty({ type: SchoolExternalToolConfigurationStatusResponse }) status: SchoolExternalToolConfigurationStatusResponse; - @ApiPropertyOptional() - logoUrl?: string; - constructor(response: SchoolExternalToolResponse) { this.id = response.id; this.name = response.name; this.toolId = response.toolId; this.schoolId = response.schoolId; + this.isDeactivated = response.isDeactivated; this.parameters = response.parameters; this.status = response.status; - this.logoUrl = response.logoUrl; } } diff --git a/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts b/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts index 44b1754ea9a..abfff864d11 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts @@ -15,14 +15,13 @@ import { import { ValidationError } from '@shared/common'; import { LegacyLogger } from '@src/core/logger'; import { ExternalToolSearchListResponse } from '../../external-tool/controller/dto'; -import { SchoolExternalTool, SchoolExternalToolMetadata } from '../domain'; +import { SchoolExternalTool, SchoolExternalToolMetadata, SchoolExternalToolProps } from '../domain'; import { SchoolExternalToolMetadataMapper, SchoolExternalToolRequestMapper, SchoolExternalToolResponseMapper, } from '../mapper'; import { SchoolExternalToolUc } from '../uc'; -import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; import { SchoolExternalToolIdParams, SchoolExternalToolMetadataResponse, @@ -36,12 +35,7 @@ import { @Authenticate('jwt') @Controller('tools/school-external-tools') export class ToolSchoolController { - constructor( - private readonly schoolExternalToolUc: SchoolExternalToolUc, - private readonly responseMapper: SchoolExternalToolResponseMapper, - private readonly requestMapper: SchoolExternalToolRequestMapper, - private readonly logger: LegacyLogger - ) {} + constructor(private readonly schoolExternalToolUc: SchoolExternalToolUc, private readonly logger: LegacyLogger) {} @Get() @ApiFoundResponse({ description: 'SchoolExternalTools has been found.', type: ExternalToolSearchListResponse }) @@ -55,7 +49,8 @@ export class ToolSchoolController { const found: SchoolExternalTool[] = await this.schoolExternalToolUc.findSchoolExternalTools(currentUser.userId, { schoolId: schoolExternalToolParams.schoolId, }); - const response: SchoolExternalToolSearchListResponse = this.responseMapper.mapToSearchListResponse(found); + const response: SchoolExternalToolSearchListResponse = + SchoolExternalToolResponseMapper.mapToSearchListResponse(found); return response; } @@ -71,7 +66,8 @@ export class ToolSchoolController { currentUser.userId, params.schoolExternalToolId ); - const mapped: SchoolExternalToolResponse = this.responseMapper.mapToSchoolExternalToolResponse(schoolExternalTool); + const mapped: SchoolExternalToolResponse = + SchoolExternalToolResponseMapper.mapToSchoolExternalToolResponse(schoolExternalTool); return mapped; } @@ -86,14 +82,16 @@ export class ToolSchoolController { @Param() params: SchoolExternalToolIdParams, @Body() body: SchoolExternalToolPostParams ): Promise { - const schoolExternalToolDto: SchoolExternalToolDto = this.requestMapper.mapSchoolExternalToolRequest(body); + const schoolExternalToolDto: SchoolExternalToolProps = + SchoolExternalToolRequestMapper.mapSchoolExternalToolRequest(body); const updated: SchoolExternalTool = await this.schoolExternalToolUc.updateSchoolExternalTool( currentUser.userId, params.schoolExternalToolId, schoolExternalToolDto ); - const mapped: SchoolExternalToolResponse = this.responseMapper.mapToSchoolExternalToolResponse(updated); + const mapped: SchoolExternalToolResponse = + SchoolExternalToolResponseMapper.mapToSchoolExternalToolResponse(updated); this.logger.debug(`SchoolExternalTool with id ${mapped.id} was updated by user with id ${currentUser.userId}`); return mapped; } @@ -127,7 +125,8 @@ export class ToolSchoolController { @CurrentUser() currentUser: ICurrentUser, @Body() body: SchoolExternalToolPostParams ): Promise { - const schoolExternalToolDto: SchoolExternalToolDto = this.requestMapper.mapSchoolExternalToolRequest(body); + const schoolExternalToolDto: SchoolExternalToolProps = + SchoolExternalToolRequestMapper.mapSchoolExternalToolRequest(body); const createdSchoolExternalToolDO: SchoolExternalTool = await this.schoolExternalToolUc.createSchoolExternalTool( currentUser.userId, @@ -135,7 +134,7 @@ export class ToolSchoolController { ); const response: SchoolExternalToolResponse = - this.responseMapper.mapToSchoolExternalToolResponse(createdSchoolExternalToolDO); + SchoolExternalToolResponseMapper.mapToSchoolExternalToolResponse(createdSchoolExternalToolDO); this.logger.debug(`SchoolExternalTool with id ${response.id} was created by user with id ${currentUser.userId}`); diff --git a/apps/server/src/modules/tool/school-external-tool/domain/index.ts b/apps/server/src/modules/tool/school-external-tool/domain/index.ts index c0716afd84b..2a85b86634b 100644 --- a/apps/server/src/modules/tool/school-external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/domain/index.ts @@ -1,3 +1,4 @@ export * from './school-external-tool.do'; export * from './school-external-tool.ref'; export * from './school-external-tool-metadata'; +export { SchoolExternalToolConfigurationStatus } from './school-external-tool-configuration-status'; diff --git a/apps/server/src/modules/tool/school-external-tool/controller/domain/school-external-tool-configuration-status.ts b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-configuration-status.ts similarity index 70% rename from apps/server/src/modules/tool/school-external-tool/controller/domain/school-external-tool-configuration-status.ts rename to apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-configuration-status.ts index b8dcfcd13d3..104dbed6197 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/domain/school-external-tool-configuration-status.ts +++ b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-configuration-status.ts @@ -1,10 +1,10 @@ export class SchoolExternalToolConfigurationStatus { isOutdatedOnScopeSchool: boolean; - isDeactivated: boolean; + isGloballyDeactivated: boolean; constructor(props: SchoolExternalToolConfigurationStatus) { this.isOutdatedOnScopeSchool = props.isOutdatedOnScopeSchool; - this.isDeactivated = props.isDeactivated; + this.isGloballyDeactivated = props.isGloballyDeactivated; } } diff --git a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts index bad765ea17f..4d8ed9b8a9f 100644 --- a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts +++ b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts @@ -1,6 +1,6 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { CustomParameterEntry } from '../../common/domain'; -import { SchoolExternalToolConfigurationStatus } from '../controller/dto'; +import { SchoolExternalToolConfigurationStatus } from './school-external-tool-configuration-status'; export interface SchoolExternalToolProps extends AuthorizableObject { id: string; @@ -13,6 +13,8 @@ export interface SchoolExternalToolProps extends AuthorizableObject { parameters: CustomParameterEntry[]; + isDeactivated: boolean; + status?: SchoolExternalToolConfigurationStatus; } @@ -37,11 +39,15 @@ export class SchoolExternalTool extends DomainObject { return this.props.parameters; } - get status(): SchoolExternalToolConfigurationStatus | undefined { - return this.props.status; + get isDeactivated(): boolean { + return this.props.isDeactivated; + } + + get status(): SchoolExternalToolConfigurationStatus { + return this.props.status ?? { isOutdatedOnScopeSchool: false, isGloballyDeactivated: false }; } - set status(value: SchoolExternalToolConfigurationStatus | undefined) { + set status(value: SchoolExternalToolConfigurationStatus) { this.props.status = value; } } diff --git a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.spec.ts b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.spec.ts deleted file mode 100644 index 87a3985ac31..00000000000 --- a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { setupEntities } from '@shared/testing'; -import { schoolExternalToolConfigurationStatusEntityFactory } from '../testing/school-external-tool-configuration-status-entity.factory'; -import { SchoolExternalToolConfigurationStatusEntity } from './school-external-tool-configuration-status.entity'; - -describe('SchoolExternalToolConfigurationStatusEntity', () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('constructor', () => { - it('should throw an error by empty constructor', () => { - // @ts-expect-error: Test case - const test = () => new SchoolExternalToolConfigurationStatusEntity(); - expect(test).toThrow(); - }); - - it('should create a school external tool configuration status by passing required properties', () => { - const schoolExternalToolConfigurationStatusEntity: SchoolExternalToolConfigurationStatusEntity = - schoolExternalToolConfigurationStatusEntityFactory.build(); - expect( - schoolExternalToolConfigurationStatusEntity instanceof SchoolExternalToolConfigurationStatusEntity - ).toEqual(false); - }); - - it('should set school external tool status', () => { - const schoolExternalToolConfigurationStatusEntity: SchoolExternalToolConfigurationStatusEntity = - new SchoolExternalToolConfigurationStatusEntity({ - isDeactivated: true, - isOutdatedOnScopeSchool: false, - }); - - expect(schoolExternalToolConfigurationStatusEntity).toEqual({ - isDeactivated: true, - isOutdatedOnScopeSchool: false, - }); - }); - }); -}); diff --git a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.ts b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.ts deleted file mode 100644 index ea071f996e1..00000000000 --- a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool-configuration-status.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Embeddable, Property } from '@mikro-orm/core'; - -@Embeddable() -export class SchoolExternalToolConfigurationStatusEntity { - @Property() - isOutdatedOnScopeSchool: boolean; - - @Property() - isDeactivated: boolean; - - constructor(props: SchoolExternalToolConfigurationStatusEntity) { - this.isOutdatedOnScopeSchool = props.isOutdatedOnScopeSchool; - this.isDeactivated = props.isDeactivated; - } -} diff --git a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts deleted file mode 100644 index d1f03b47d80..00000000000 --- a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { schoolEntityFactory, setupEntities } from '@shared/testing'; -import { CustomParameterLocation, CustomParameterScope, CustomParameterType, ToolConfigType } from '../../common/enum'; -import { CustomParameterEntity, ExternalToolConfigEntity, ExternalToolEntity } from '../../external-tool/entity'; -import { - basicToolConfigFactory, - customParameterEntityFactory, - externalToolEntityFactory, -} from '../../external-tool/testing'; -import { schoolExternalToolConfigurationStatusEntityFactory, schoolExternalToolEntityFactory } from '../testing'; -import { SchoolExternalToolEntity } from './school-external-tool.entity'; - -describe('SchoolExternalToolEntity', () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('constructor', () => { - it('should throw an error by empty constructor', () => { - // @ts-expect-error: Test case - const test = () => new SchoolExternalToolEntity(); - expect(test).toThrow(); - }); - - it('should create an external school Tool by passing required properties', () => { - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId(); - expect(schoolExternalToolEntity instanceof SchoolExternalToolEntity).toEqual(true); - }); - - it('should set schoolParameters to empty when is undefined', () => { - const externalToolConfigEntity: ExternalToolConfigEntity = basicToolConfigFactory.buildWithId({ - type: ToolConfigType.OAUTH2, - baseUrl: 'mockBaseUrl', - }); - const customParameter: CustomParameterEntity = customParameterEntityFactory.build({ - name: 'parameterName', - displayName: 'User Friendly Name', - default: 'mock', - location: CustomParameterLocation.PATH, - scope: CustomParameterScope.SCHOOL, - type: CustomParameterType.STRING, - regex: 'mockRegex', - regexComment: 'mockComment', - isOptional: false, - isProtected: false, - }); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ - name: 'toolName', - url: 'mockUrl', - logoUrl: 'mockLogoUrl', - config: externalToolConfigEntity, - parameters: [customParameter], - isHidden: true, - openNewTab: true, - isDeactivated: false, - }); - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - tool: externalToolEntity, - school: schoolEntityFactory.buildWithId(), - schoolParameters: [], - status: schoolExternalToolConfigurationStatusEntityFactory.build(), - }); - - expect(schoolExternalToolEntity.schoolParameters).toEqual([]); - }); - - it('should set school external tool configuration status', () => { - const externalToolConfigEntity: ExternalToolConfigEntity = basicToolConfigFactory.buildWithId({ - type: ToolConfigType.OAUTH2, - baseUrl: 'mockBaseUrl', - }); - const customParameter: CustomParameterEntity = customParameterEntityFactory.build({ - name: 'parameterName', - displayName: 'User Friendly Name', - default: 'mock', - location: CustomParameterLocation.PATH, - scope: CustomParameterScope.SCHOOL, - type: CustomParameterType.STRING, - regex: 'mockRegex', - regexComment: 'mockComment', - isOptional: false, - isProtected: false, - }); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ - name: 'toolName', - url: 'mockUrl', - logoUrl: 'mockLogoUrl', - config: externalToolConfigEntity, - parameters: [customParameter], - isHidden: true, - openNewTab: true, - isDeactivated: false, - }); - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - tool: externalToolEntity, - school: schoolEntityFactory.buildWithId(), - schoolParameters: [], - status: schoolExternalToolConfigurationStatusEntityFactory.build(), - }); - - expect(schoolExternalToolEntity.status).toEqual({ isDeactivated: false, isOutdatedOnScopeSchool: false }); - }); - }); -}); 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 cd798abfccd..a43539d371b 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 @@ -1,10 +1,9 @@ -import { Embedded, Entity, ManyToOne } from '@mikro-orm/core'; +import { Embedded, Entity, ManyToOne, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { SchoolEntity } from '@shared/domain/entity/school.entity'; import { EntityId } from '@shared/domain/types'; import { CustomParameterEntryEntity } from '../../common/entity'; import { ExternalToolEntity } from '../../external-tool/entity'; -import { SchoolExternalToolConfigurationStatusEntity } from './school-external-tool-configuration-status.entity'; export interface SchoolExternalToolEntityProps { id?: EntityId; @@ -15,7 +14,7 @@ export interface SchoolExternalToolEntityProps { schoolParameters?: CustomParameterEntryEntity[]; - status?: SchoolExternalToolConfigurationStatusEntity; + isDeactivated: boolean; } @Entity({ tableName: 'school-external-tools' }) @@ -29,8 +28,8 @@ export class SchoolExternalToolEntity extends BaseEntityWithTimestamps { @Embedded(() => CustomParameterEntryEntity, { array: true }) schoolParameters: CustomParameterEntryEntity[]; - @Embedded(() => SchoolExternalToolConfigurationStatusEntity, { object: true, nullable: true }) - status?: SchoolExternalToolConfigurationStatusEntity; + @Property() + isDeactivated: boolean; constructor(props: SchoolExternalToolEntityProps) { super(); @@ -40,6 +39,6 @@ export class SchoolExternalToolEntity extends BaseEntityWithTimestamps { this.tool = props.tool; this.school = props.school; this.schoolParameters = props.schoolParameters ?? []; - this.status = props.status; + this.isDeactivated = props.isDeactivated; } } diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/index.ts b/apps/server/src/modules/tool/school-external-tool/mapper/index.ts index 4b663b7b50b..48cd0b13a20 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/index.ts @@ -1,4 +1,3 @@ export * from './school-external-tool-request.mapper'; export * from './school-external-tool-response.mapper'; export * from './school-external-tool-metadata.mapper'; -export * from './school-external-tool-status-response.mapper'; diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.spec.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.spec.ts index 9fe3f7f1d33..abab0048a6e 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.spec.ts @@ -1,11 +1,8 @@ import { CustomParameterEntryParam, SchoolExternalToolPostParams } from '../controller/dto'; -import { schoolExternalToolConfigurationStatusFactory } from '../testing'; -import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; +import { SchoolExternalToolProps } from '../domain'; import { SchoolExternalToolRequestMapper } from './school-external-tool-request.mapper'; describe('SchoolExternalToolRequestMapper', () => { - const mapper: SchoolExternalToolRequestMapper = new SchoolExternalToolRequestMapper(); - describe('mapSchoolExternalToolRequest', () => { describe('when SchoolExternalToolPostParams is given', () => { const setup = () => { @@ -29,14 +26,15 @@ describe('SchoolExternalToolRequestMapper', () => { it('should return an schoolExternalTool', () => { const { param, params } = setup(); - const schoolExternalToolDto: SchoolExternalToolDto = mapper.mapSchoolExternalToolRequest(params); + const schoolExternalToolDto: SchoolExternalToolProps = + SchoolExternalToolRequestMapper.mapSchoolExternalToolRequest(params); - expect(schoolExternalToolDto).toEqual({ + expect(schoolExternalToolDto).toEqual({ id: expect.any(String), toolId: params.toolId, parameters: [{ name: param.name, value: param.value }], schoolId: params.schoolId, - status: schoolExternalToolConfigurationStatusFactory.build({ isDeactivated: true }), + isDeactivated: true, }); }); }); @@ -58,14 +56,15 @@ describe('SchoolExternalToolRequestMapper', () => { it('should return an schoolExternalTool without parameter', () => { const { params } = setup(); - const schoolExternalToolDto: SchoolExternalToolDto = mapper.mapSchoolExternalToolRequest(params); + const schoolExternalToolDto: SchoolExternalToolProps = + SchoolExternalToolRequestMapper.mapSchoolExternalToolRequest(params); - expect(schoolExternalToolDto).toEqual({ + expect(schoolExternalToolDto).toEqual({ id: expect.any(String), toolId: params.toolId, parameters: [], schoolId: params.schoolId, - status: schoolExternalToolConfigurationStatusFactory.build(), + isDeactivated: false, }); }); }); 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 6e05bc21ed2..b0061ac44ae 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,29 +1,20 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { Injectable } from '@nestjs/common'; import { CustomParameterEntry } from '../../common/domain'; -import { - CustomParameterEntryParam, - SchoolExternalToolConfigurationStatus, - SchoolExternalToolPostParams, -} from '../controller/dto'; -import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; +import { CustomParameterEntryParam, SchoolExternalToolPostParams } from '../controller/dto'; +import { SchoolExternalToolProps } from '../domain'; -@Injectable() export class SchoolExternalToolRequestMapper { - mapSchoolExternalToolRequest(request: SchoolExternalToolPostParams): SchoolExternalToolDto { + public static mapSchoolExternalToolRequest(request: SchoolExternalToolPostParams): SchoolExternalToolProps { return { id: new ObjectId().toHexString(), toolId: request.toolId, schoolId: request.schoolId, - parameters: this.mapRequestToCustomParameterEntryDO(request.parameters ?? []), - status: new SchoolExternalToolConfigurationStatus({ - isOutdatedOnScopeSchool: false, - isDeactivated: request.isDeactivated, - }), + parameters: SchoolExternalToolRequestMapper.mapRequestToCustomParameterEntryDO(request.parameters ?? []), + isDeactivated: request.isDeactivated, }; } - private mapRequestToCustomParameterEntryDO( + private static mapRequestToCustomParameterEntryDO( customParameterParams: CustomParameterEntryParam[] ): CustomParameterEntry[] { return customParameterParams.map((customParameterParam: CustomParameterEntryParam) => { diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts deleted file mode 100644 index a3dae02d9ba..00000000000 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { SchoolExternalToolResponse, SchoolExternalToolSearchListResponse } from '../controller/dto'; -import { SchoolExternalTool } from '../domain'; -import { - schoolExternalToolConfigurationStatusFactory, - schoolExternalToolConfigurationStatusResponseFactory, - schoolExternalToolFactory, -} from '../testing'; -import { SchoolExternalToolResponseMapper } from './school-external-tool-response.mapper'; - -describe('SchoolExternalToolResponseMapper', () => { - let mapper: SchoolExternalToolResponseMapper; - - beforeAll(() => { - mapper = new SchoolExternalToolResponseMapper(); - }); - - describe('mapToSearchListResponse', () => { - it('should return a schoolExternalToolResponse', () => { - const response: SchoolExternalToolSearchListResponse = mapper.mapToSearchListResponse([]); - - expect(response).toBeInstanceOf(SchoolExternalToolSearchListResponse); - }); - - describe('when parameter are given', () => { - const setup = () => { - const do1: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); - const do2: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); - do2.status = undefined; - - const dos: SchoolExternalTool[] = [do1, do2]; - - return { - dos, - do1, - do2, - }; - }; - - it('should map domain objects correctly', () => { - const { dos, do1, do2 } = setup(); - - const response: SchoolExternalToolSearchListResponse = mapper.mapToSearchListResponse(dos); - - expect(response.data).toEqual( - expect.objectContaining([ - { - id: do1.id, - name: do1.name as string, - schoolId: do1.schoolId, - toolId: do1.toolId, - parameters: [ - { - name: do1.parameters[0].name, - value: do1.parameters[0].value, - }, - ], - status: schoolExternalToolConfigurationStatusResponseFactory.build({ - isOutdatedOnScopeSchool: false, - isDeactivated: false, - }), - }, - { - id: do2.id, - name: do2.name as string, - schoolId: do2.schoolId, - toolId: do2.toolId, - parameters: [ - { - name: do2.parameters[0].name, - value: do2.parameters[0].value, - }, - ], - status: schoolExternalToolConfigurationStatusFactory.build({ - isOutdatedOnScopeSchool: false, - isDeactivated: false, - }), - }, - ]) - ); - }); - }); - - describe('when optional parameter are missing', () => { - const setup = () => { - const do1: SchoolExternalTool = schoolExternalToolFactory.build({ id: undefined }); - do1.name = undefined; - do1.status = undefined; - - const do2: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); - - const dos: SchoolExternalTool[] = [do1, do2]; - - return { - dos, - }; - }; - - it('should set defaults', () => { - const { dos } = setup(); - - const response: SchoolExternalToolSearchListResponse = mapper.mapToSearchListResponse(dos); - - expect(response.data[0]).toEqual( - expect.objectContaining({ - id: '', - name: '', - status: schoolExternalToolConfigurationStatusResponseFactory.build({ - isOutdatedOnScopeSchool: false, - isDeactivated: false, - }), - }) - ); - }); - }); - }); -}); diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts index 71de9168de5..50e75ecd37d 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts @@ -1,36 +1,39 @@ -import { Injectable } from '@nestjs/common'; import { CustomParameterEntry } from '../../common/domain'; import { CustomParameterEntryResponse, + SchoolExternalToolConfigurationStatusResponse, SchoolExternalToolResponse, SchoolExternalToolSearchListResponse, } from '../controller/dto'; import { SchoolExternalTool } from '../domain'; -import { SchoolToolConfigurationStatusResponseMapper } from './school-external-tool-status-response.mapper'; -@Injectable() export class SchoolExternalToolResponseMapper { - mapToSearchListResponse(externalTools: SchoolExternalTool[]): SchoolExternalToolSearchListResponse { + static mapToSearchListResponse(externalTools: SchoolExternalTool[]): SchoolExternalToolSearchListResponse { const responses: SchoolExternalToolResponse[] = externalTools.map((toolDO: SchoolExternalTool) => this.mapToSchoolExternalToolResponse(toolDO) ); + return new SchoolExternalToolSearchListResponse(responses); } - mapToSchoolExternalToolResponse(schoolExternalTool: SchoolExternalTool): SchoolExternalToolResponse { - return { - id: schoolExternalTool.id ?? '', + static mapToSchoolExternalToolResponse(schoolExternalTool: SchoolExternalTool): SchoolExternalToolResponse { + const response: SchoolExternalToolResponse = new SchoolExternalToolResponse({ + id: schoolExternalTool.id, name: schoolExternalTool.name ?? '', toolId: schoolExternalTool.toolId, schoolId: schoolExternalTool.schoolId, - parameters: this.mapToCustomParameterEntryResponse(schoolExternalTool.parameters), - status: SchoolToolConfigurationStatusResponseMapper.mapToResponse( - schoolExternalTool.status ?? { isOutdatedOnScopeSchool: false, isDeactivated: false } - ), - }; + isDeactivated: schoolExternalTool.isDeactivated, + parameters: SchoolExternalToolResponseMapper.mapToCustomParameterEntryResponse(schoolExternalTool.parameters), + status: new SchoolExternalToolConfigurationStatusResponse({ + isOutdatedOnScopeSchool: schoolExternalTool.status.isOutdatedOnScopeSchool, + isGloballyDeactivated: schoolExternalTool.status.isGloballyDeactivated, + }), + }); + + return response; } - private mapToCustomParameterEntryResponse(entries: CustomParameterEntry[]): CustomParameterEntryResponse[] { + private static mapToCustomParameterEntryResponse(entries: CustomParameterEntry[]): CustomParameterEntryResponse[] { return entries.map( (entry: CustomParameterEntry): CustomParameterEntry => new CustomParameterEntryResponse({ diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-status-response.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-status-response.mapper.ts deleted file mode 100644 index 001efe071c0..00000000000 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-status-response.mapper.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SchoolExternalToolConfigurationStatus } from '../controller/dto'; -import { SchoolExternalToolConfigurationStatusResponse } from '../controller/dto/school-external-tool-configuration.response'; - -export class SchoolToolConfigurationStatusResponseMapper { - static mapToResponse(status: SchoolExternalToolConfigurationStatus): SchoolExternalToolConfigurationStatusResponse { - const configurationStatus: SchoolExternalToolConfigurationStatusResponse = - new SchoolExternalToolConfigurationStatusResponse({ - isOutdatedOnScopeSchool: status.isOutdatedOnScopeSchool, - isDeactivated: status.isDeactivated, - }); - - return configurationStatus; - } -} 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 f0e8f89640f..9f3e50b6b9a 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 @@ -1,14 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; +import { ValidationError } from '@shared/common'; import { SchoolExternalToolRepo } from '@shared/repo'; +import { CommonToolValidationService } from '../../common/service'; import { ExternalToolService } from '../../external-tool'; -import { ExternalTool } from '../../external-tool/domain'; +import { type ExternalTool } from '../../external-tool/domain'; import { externalToolFactory } from '../../external-tool/testing'; -import { SchoolExternalTool } from '../domain'; -import { schoolExternalToolConfigurationStatusFactory, schoolExternalToolFactory } from '../testing'; +import { SchoolExternalTool, SchoolExternalToolConfigurationStatus } from '../domain'; +import { schoolExternalToolFactory } from '../testing'; import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; -import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; import { SchoolExternalToolService } from './school-external-tool.service'; describe(SchoolExternalToolService.name, () => { @@ -17,7 +17,7 @@ describe(SchoolExternalToolService.name, () => { let schoolExternalToolRepo: DeepMocked; let externalToolService: DeepMocked; - let schoolExternalToolValidationService: DeepMocked; + let commonToolValidationService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -32,8 +32,8 @@ describe(SchoolExternalToolService.name, () => { useValue: createMock(), }, { - provide: SchoolExternalToolValidationService, - useValue: createMock(), + provide: CommonToolValidationService, + useValue: createMock(), }, ], }).compile(); @@ -41,293 +41,178 @@ describe(SchoolExternalToolService.name, () => { service = module.get(SchoolExternalToolService); schoolExternalToolRepo = module.get(SchoolExternalToolRepo); externalToolService = module.get(ExternalToolService); - schoolExternalToolValidationService = module.get(SchoolExternalToolValidationService); + commonToolValidationService = module.get(CommonToolValidationService); }); describe('findSchoolExternalTools', () => { describe('when called with query', () => { - describe('findSchoolExternalTools', () => { - describe('when called with query', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const schoolExternalToolQuery: SchoolExternalToolQuery = { - schoolId: schoolExternalTool.schoolId, - toolId: schoolExternalTool.toolId, - isDeactivated: !!schoolExternalTool.status?.isDeactivated, - }; - - schoolExternalToolRepo.find.mockResolvedValueOnce([schoolExternalTool]); - - return { - schoolExternalTool, - schoolExternalToolId: schoolExternalTool.id, - schoolExternalToolQuery, - }; - }; - - it('should call repo with query', async () => { - const { schoolExternalTool, schoolExternalToolQuery } = setup(); - - await service.findSchoolExternalTools(schoolExternalToolQuery); - - expect(schoolExternalToolRepo.find).toHaveBeenCalledWith<[Required]>({ - schoolId: schoolExternalTool.schoolId, - toolId: schoolExternalTool.toolId, - isDeactivated: !!schoolExternalTool.status?.isDeactivated, - }); - }); - - it('should return schoolExternalTool array', async () => { - const { schoolExternalToolQuery } = setup(); - - const result: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalToolQuery); - - expect(Array.isArray(result)).toBe(true); - }); + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + name: undefined, + status: undefined, }); - }); - describe('enrichDataFromExternalTool', () => { - describe('when schoolExternalTool is given', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const externalTool: ExternalTool = externalToolFactory.buildWithId(); + const schoolExternalToolQuery: SchoolExternalToolQuery = { + schoolId: schoolExternalTool.schoolId, + toolId: schoolExternalTool.toolId, + isDeactivated: schoolExternalTool.isDeactivated, + }; - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); + schoolExternalToolRepo.find.mockResolvedValueOnce([schoolExternalTool]); + externalToolService.findById.mockResolvedValueOnce(externalTool); + commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); - return { - schoolExternalTool, - }; - }; + return { + externalTool, + schoolExternalTool, + schoolExternalToolQuery, + }; + }; - it('should call the externalToolService', async () => { - const { schoolExternalTool } = setup(); + it('should call repo with query', async () => { + const { schoolExternalTool, schoolExternalToolQuery } = setup(); - await service.findSchoolExternalTools(schoolExternalTool); + await service.findSchoolExternalTools(schoolExternalToolQuery); - expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); - }); + expect(schoolExternalToolRepo.find).toHaveBeenCalledWith<[Required]>({ + schoolId: schoolExternalTool.schoolId, + toolId: schoolExternalTool.toolId, + isDeactivated: schoolExternalTool.isDeactivated, }); + }); - describe('when determine status', () => { - describe('when 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(); - - 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( - schoolExternalToolConfigurationStatusFactory.build({ - isOutdatedOnScopeSchool: false, - }) - ); - }); - - it('should return non deactivated tool status', async () => { - const { schoolExternalTool } = setup(); - - const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( - schoolExternalTool - ); - - expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); - expect(schoolExternalToolDOs[0].status).toEqual( - schoolExternalToolConfigurationStatusFactory.build({ - isDeactivated: false, - }) - ); - }); - }); - - describe('when 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); - - return { - schoolExternalTool, - }; - }; - - it('should return outdated tool status', async () => { - const { schoolExternalTool } = setup(); - - const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( - schoolExternalTool - ); - - expect(schoolExternalToolValidationService.validate).toHaveBeenCalledWith(schoolExternalTool); - expect(schoolExternalToolDOs[0].status).toEqual( - schoolExternalToolConfigurationStatusFactory.build({ - isOutdatedOnScopeSchool: true, - }) - ); - }); - }); - - describe('when schoolExternalTool is deactivated', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - status: schoolExternalToolConfigurationStatusFactory.build({ isDeactivated: true }), - }); - const externalTool: ExternalTool = externalToolFactory.buildWithId(); - - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); - schoolExternalToolValidationService.validate.mockRejectedValue(Promise.resolve()); - - return { - schoolExternalTool, - }; - }; - - it('should return deactivated tool status true', async () => { - const { schoolExternalTool } = setup(); - - const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( - schoolExternalTool - ); - - expect(schoolExternalToolDOs[0].status).toEqual( - schoolExternalToolConfigurationStatusFactory.build({ - isDeactivated: true, - isOutdatedOnScopeSchool: true, - }) - ); - }); - }); - - describe('when externalTool is deactivated', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const externalTool: ExternalTool = externalToolFactory.buildWithId({ isDeactivated: true }); - - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); - schoolExternalToolValidationService.validate.mockRejectedValue(Promise.resolve()); - - return { - schoolExternalTool, - }; - }; - - it('should return deactivated tool status true', async () => { - const { schoolExternalTool } = setup(); - - const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools( - schoolExternalTool - ); - - expect(schoolExternalToolDOs[0].status).toEqual( - schoolExternalToolConfigurationStatusFactory.build({ - isDeactivated: true, - isOutdatedOnScopeSchool: true, - }) - ); - }); - }); - }); + it('should return schoolExternalTool array with enriched data', async () => { + const { schoolExternalToolQuery, schoolExternalTool, externalTool } = setup(); + + const result: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalToolQuery); + + expect(result).toEqual([ + new SchoolExternalTool({ + ...schoolExternalTool.getProps(), + name: externalTool.name, + status: new SchoolExternalToolConfigurationStatus({ + isGloballyDeactivated: externalTool.isDeactivated, + isOutdatedOnScopeSchool: true, + }), + }), + ]); }); + }); + }); - describe('deleteSchoolExternalToolById', () => { - describe('when schoolExternalToolId is given', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const externalTool: ExternalTool = externalToolFactory.buildWithId(); + describe('deleteSchoolExternalToolById', () => { + describe('when schoolExternalToolId is given', () => { + const setup = () => { + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const externalTool: ExternalTool = externalToolFactory.buildWithId(); - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); + schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); + externalToolService.findById.mockResolvedValue(externalTool); - return { - schoolExternalToolId: schoolExternalTool.id, - }; - }; + return { + schoolExternalToolId: schoolExternalTool.id, + }; + }; - it('should call the schoolExternalToolRepo', () => { - const { schoolExternalToolId } = setup(); + it('should call the schoolExternalToolRepo', () => { + const { schoolExternalToolId } = setup(); - service.deleteSchoolExternalToolById(schoolExternalToolId); + service.deleteSchoolExternalToolById(schoolExternalToolId); - expect(schoolExternalToolRepo.deleteById).toHaveBeenCalledWith(schoolExternalToolId); - }); - }); + expect(schoolExternalToolRepo.deleteById).toHaveBeenCalledWith(schoolExternalToolId); }); + }); + }); - describe('findById', () => { - describe('when schoolExternalToolId is given', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const externalTool: ExternalTool = externalToolFactory.buildWithId(); + describe('findById', () => { + describe('when schoolExternalToolId is given', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + name: undefined, + status: undefined, + }); - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); + schoolExternalToolRepo.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); + commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); - return { - schoolExternalToolId: schoolExternalTool.id, - }; - }; + return { + schoolExternalTool, + externalTool, + }; + }; - it('should call schoolExternalToolRepo.findById', async () => { - const { schoolExternalToolId } = setup(); + it('should call schoolExternalToolRepo.findById', async () => { + const { schoolExternalTool } = setup(); - await service.findById(schoolExternalToolId); + await service.findById(schoolExternalTool.id); - expect(schoolExternalToolRepo.findById).toHaveBeenCalledWith(schoolExternalToolId); - }); - }); + expect(schoolExternalToolRepo.findById).toHaveBeenCalledWith(schoolExternalTool.id); }); - describe('saveSchoolExternalTool', () => { - describe('when schoolExternalTool is given', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const externalTool: ExternalTool = externalToolFactory.buildWithId(); - - schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findById.mockResolvedValue(externalTool); + it('should return the schoolExternalTool with enriched data', async () => { + const { schoolExternalTool, externalTool } = setup(); + + const result = await service.findById(schoolExternalTool.id); + + expect(result).toEqual( + new SchoolExternalTool({ + ...schoolExternalTool.getProps(), + name: externalTool.name, + status: new SchoolExternalToolConfigurationStatus({ + isGloballyDeactivated: externalTool.isDeactivated, + isOutdatedOnScopeSchool: true, + }), + }) + ); + }); + }); + }); - return { - schoolExternalTool, - }; - }; + describe('saveSchoolExternalTool', () => { + describe('when schoolExternalTool is given', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + name: undefined, + status: undefined, + }); - it('should call schoolExternalToolRepo.save', async () => { - const { schoolExternalTool } = setup(); + schoolExternalToolRepo.save.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); + commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); - await service.saveSchoolExternalTool(schoolExternalTool); + return { + schoolExternalTool, + externalTool, + }; + }; - expect(schoolExternalToolRepo.save).toHaveBeenCalledWith(schoolExternalTool); - }); + it('should call schoolExternalToolRepo.save', async () => { + const { schoolExternalTool } = setup(); - it('should enrich data from externalTool', async () => { - const { schoolExternalTool } = setup(); + await service.saveSchoolExternalTool(schoolExternalTool); - await service.saveSchoolExternalTool(schoolExternalTool); + expect(schoolExternalToolRepo.save).toHaveBeenCalledWith(schoolExternalTool); + }); - expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); - }); - }); + it('should return the schoolExternalTool with enriched data', async () => { + const { schoolExternalTool, externalTool } = setup(); + + const result = await service.saveSchoolExternalTool(schoolExternalTool); + + expect(result).toEqual( + new SchoolExternalTool({ + ...schoolExternalTool.getProps(), + name: externalTool.name, + status: new SchoolExternalToolConfigurationStatus({ + isGloballyDeactivated: externalTool.isDeactivated, + isOutdatedOnScopeSchool: true, + }), + }) + ); }); }); }); 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 0a05ceb5685..978f0aa4ee5 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,23 +1,25 @@ import { Injectable } from '@nestjs/common'; +import { ValidationError } from '@shared/common'; import { EntityId } from '@shared/domain/types'; import { SchoolExternalToolRepo } from '@shared/repo'; +import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; -import { SchoolExternalToolConfigurationStatus } from '../controller/dto'; -import { SchoolExternalTool } from '../domain'; +import { SchoolExternalTool, SchoolExternalToolConfigurationStatus } from '../domain'; import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; -import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; @Injectable() export class SchoolExternalToolService { constructor( private readonly schoolExternalToolRepo: SchoolExternalToolRepo, private readonly externalToolService: ExternalToolService, - private readonly schoolExternalToolValidationService: SchoolExternalToolValidationService + private readonly commonToolValidationService: CommonToolValidationService ) {} public async findById(schoolExternalToolId: EntityId): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolRepo.findById(schoolExternalToolId); + let schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolRepo.findById(schoolExternalToolId); + + schoolExternalTool = await this.enrichWithDataFromExternalTool(schoolExternalTool); return schoolExternalTool; } @@ -36,45 +38,45 @@ export class SchoolExternalToolService { private async enrichWithDataFromExternalTools(tools: SchoolExternalTool[]): Promise { const enrichedTools: SchoolExternalTool[] = await Promise.all( - tools.map(async (tool: SchoolExternalTool): Promise => this.enrichDataFromExternalTool(tool)) + tools.map( + async (tool: SchoolExternalTool): Promise => this.enrichWithDataFromExternalTool(tool) + ) ); return enrichedTools; } - private async enrichDataFromExternalTool(tool: SchoolExternalTool): Promise { + private async enrichWithDataFromExternalTool(tool: SchoolExternalTool): Promise { const externalTool: ExternalTool = await this.externalToolService.findById(tool.toolId); - const status: SchoolExternalToolConfigurationStatus = await this.determineSchoolToolStatus(tool, externalTool); + const status: SchoolExternalToolConfigurationStatus = this.determineSchoolToolStatus(tool, externalTool); + const schoolExternalTool: SchoolExternalTool = new SchoolExternalTool({ - id: tool.id, - toolId: tool.toolId, - schoolId: tool.schoolId, - parameters: tool.parameters, - status, + ...tool.getProps(), name: externalTool.name, + status, }); return schoolExternalTool; } - private async determineSchoolToolStatus( + private determineSchoolToolStatus( tool: SchoolExternalTool, externalTool: ExternalTool - ): Promise { - const status: SchoolExternalToolConfigurationStatus = new SchoolExternalToolConfigurationStatus({ - isOutdatedOnScopeSchool: true, - isDeactivated: this.isToolDeactivated(externalTool, tool), - }); + ): SchoolExternalToolConfigurationStatus { + let isOutdatedOnScopeSchool = false; - try { - await this.schoolExternalToolValidationService.validate(tool); + const errors: ValidationError[] = this.commonToolValidationService.validateParameters(externalTool, tool); - status.isOutdatedOnScopeSchool = false; - - return status; - } catch (err) { - return status; + if (errors.length) { + isOutdatedOnScopeSchool = true; } + + const status: SchoolExternalToolConfigurationStatus = new SchoolExternalToolConfigurationStatus({ + isOutdatedOnScopeSchool, + isGloballyDeactivated: externalTool.isDeactivated, + }); + + return status; } public deleteSchoolExternalToolById(schoolExternalToolId: EntityId): void { @@ -83,16 +85,9 @@ export class SchoolExternalToolService { public async saveSchoolExternalTool(schoolExternalTool: SchoolExternalTool): Promise { let createdSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolRepo.save(schoolExternalTool); - createdSchoolExternalTool = await this.enrichDataFromExternalTool(createdSchoolExternalTool); - - return createdSchoolExternalTool; - } - private isToolDeactivated(externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool) { - if (externalTool.isDeactivated || schoolExternalTool.status?.isDeactivated) { - return true; - } + createdSchoolExternalTool = await this.enrichWithDataFromExternalTool(createdSchoolExternalTool); - return false; + return createdSchoolExternalTool; } } diff --git a/apps/server/src/modules/tool/school-external-tool/testing/index.ts b/apps/server/src/modules/tool/school-external-tool/testing/index.ts index 2c15f549607..55b1de1dbfa 100644 --- a/apps/server/src/modules/tool/school-external-tool/testing/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/testing/index.ts @@ -2,4 +2,3 @@ export { schoolExternalToolEntityFactory } from './school-external-tool-entity.f export { schoolExternalToolFactory } from './school-external-tool.factory'; export { schoolExternalToolConfigurationStatusFactory } from './school-external-tool-configuration-status.factory'; export { schoolExternalToolConfigurationStatusResponseFactory } from './school-external-tool-configuration-status-response.factory'; -export { schoolExternalToolConfigurationStatusEntityFactory } from './school-external-tool-configuration-status-entity.factory'; diff --git a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-entity.factory.ts b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-entity.factory.ts deleted file mode 100644 index 73b02ad773d..00000000000 --- a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-entity.factory.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Factory } from 'fishery'; -import { SchoolExternalToolConfigurationStatusEntity } from '../entity/school-external-tool-configuration-status.entity'; - -export const schoolExternalToolConfigurationStatusEntityFactory = - Factory.define(() => { - return { - isOutdatedOnScopeSchool: false, - isDeactivated: false, - }; - }); diff --git a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-response.factory.ts b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-response.factory.ts index a381fadc03b..0890f40227d 100644 --- a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-response.factory.ts +++ b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status-response.factory.ts @@ -5,6 +5,6 @@ export const schoolExternalToolConfigurationStatusResponseFactory = Factory.define(() => { return { isOutdatedOnScopeSchool: false, - isDeactivated: false, + isGloballyDeactivated: false, }; }); diff --git a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status.factory.ts b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status.factory.ts index 11b38da4bbd..f925bea5a45 100644 --- a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status.factory.ts +++ b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-configuration-status.factory.ts @@ -1,11 +1,11 @@ import { Factory } from 'fishery'; -import { SchoolExternalToolConfigurationStatus } from '../controller/dto'; +import { SchoolExternalToolConfigurationStatus } from '../domain'; export const schoolExternalToolConfigurationStatusFactory = Factory.define( () => { return { isOutdatedOnScopeSchool: false, - isDeactivated: false, + isGloballyDeactivated: false, }; } ); diff --git a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-entity.factory.ts b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-entity.factory.ts index a90ceafd0ba..383aabbb407 100644 --- a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-entity.factory.ts +++ b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool-entity.factory.ts @@ -2,7 +2,6 @@ import { externalToolEntityFactory } from '@modules/tool/external-tool/testing'; import { SchoolExternalToolEntity, SchoolExternalToolEntityProps } from '@modules/tool/school-external-tool/entity'; import { BaseFactory } from '@shared/testing/factory/base.factory'; import { schoolEntityFactory } from '@shared/testing/factory/school-entity.factory'; -import { schoolExternalToolConfigurationStatusEntityFactory } from './school-external-tool-configuration-status-entity.factory'; export const schoolExternalToolEntityFactory = BaseFactory.define< SchoolExternalToolEntity, @@ -12,6 +11,6 @@ export const schoolExternalToolEntityFactory = BaseFactory.define< tool: externalToolEntityFactory.buildWithId(), school: schoolEntityFactory.buildWithId(), schoolParameters: [{ name: 'schoolMockParameter', value: 'mockValue' }], - status: schoolExternalToolConfigurationStatusEntityFactory.build(), + isDeactivated: false, }; }); diff --git a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts index a7af0be4b53..3d979b90331 100644 --- a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts +++ b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts @@ -26,6 +26,7 @@ export const schoolExternalToolFactory = SchoolExternalToolFactory.define(School }), ], toolId: 'toolId', + isDeactivated: false, status: schoolExternalToolConfigurationStatusFactory.build(), }; }); diff --git a/apps/server/src/modules/tool/school-external-tool/uc/dto/school-external-tool.types.ts b/apps/server/src/modules/tool/school-external-tool/uc/dto/school-external-tool.types.ts index 48ba154dab0..c66f31feb57 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/dto/school-external-tool.types.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/dto/school-external-tool.types.ts @@ -1,7 +1,3 @@ -import { SchoolExternalToolProps } from '../../domain'; - -export type SchoolExternalToolDto = SchoolExternalToolProps; - export type SchoolExternalToolQueryInput = { schoolId?: string; toolId?: string; diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts index d8d4c95405c..c7861f82823 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts @@ -6,9 +6,9 @@ import { EntityId } from '@shared/domain/types'; import { School, SchoolService } from '@src/modules/school'; import { CommonToolMetadataService } from '../../common/service/common-tool-metadata.service'; import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalTool, SchoolExternalToolMetadata } from '../domain'; +import { SchoolExternalTool, SchoolExternalToolMetadata, SchoolExternalToolProps } from '../domain'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; -import { SchoolExternalToolDto, SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; +import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; @Injectable() export class SchoolExternalToolUc { @@ -38,7 +38,7 @@ export class SchoolExternalToolUc { async createSchoolExternalTool( userId: EntityId, - schoolExternalToolDto: SchoolExternalToolDto + schoolExternalToolDto: SchoolExternalToolProps ): Promise { const schoolExternalTool = new SchoolExternalTool({ ...schoolExternalToolDto }); @@ -96,7 +96,7 @@ export class SchoolExternalToolUc { async updateSchoolExternalTool( userId: EntityId, schoolExternalToolId: string, - schoolExternalToolDto: SchoolExternalToolDto + schoolExternalToolDto: SchoolExternalToolProps ): Promise { const schoolExternalTool = new SchoolExternalTool({ ...schoolExternalToolDto }); diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 067f062e7f3..783c82ac3da 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -18,7 +18,6 @@ import { ExternalToolRequestMapper, ExternalToolResponseMapper } from './externa import { ExternalToolConfigurationService } from './external-tool/service'; import { ExternalToolConfigurationUc, ExternalToolUc } from './external-tool/uc'; import { ToolSchoolController } from './school-external-tool/controller'; -import { SchoolExternalToolRequestMapper, SchoolExternalToolResponseMapper } from './school-external-tool/mapper'; import { SchoolExternalToolUc } from './school-external-tool/uc'; import { ToolConfigModule } from './tool-config.module'; import { ToolLaunchController } from './tool-launch/controller/tool-launch.controller'; @@ -55,8 +54,6 @@ import { ToolModule } from './tool.module'; ExternalToolRequestMapper, ExternalToolResponseMapper, SchoolExternalToolUc, - SchoolExternalToolResponseMapper, - SchoolExternalToolRequestMapper, ContextExternalToolUc, ToolLaunchUc, ToolReferenceUc, diff --git a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts index f0e3aec8e24..f85cfcd69a3 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts @@ -25,10 +25,7 @@ import { externalToolEntityFactory, } from '../../../external-tool/testing'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; -import { - schoolExternalToolConfigurationStatusEntityFactory, - schoolExternalToolEntityFactory, -} from '../../../school-external-tool/testing'; +import { schoolExternalToolEntityFactory } from '../../../school-external-tool/testing'; import { LaunchRequestMethod } from '../../types'; import { ContextExternalToolBodyParams, ContextExternalToolLaunchParams, ToolLaunchRequestResponse } from '../dto'; @@ -242,9 +239,7 @@ describe('ToolLaunchController (API)', () => { const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, school, - status: schoolExternalToolConfigurationStatusEntityFactory.build({ - isDeactivated: true, - }), + isDeactivated: true, }); const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalToolEntity, diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.spec.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.spec.ts index 85954491420..7502ecfdce5 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.spec.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.spec.ts @@ -6,11 +6,7 @@ import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { externalToolEntityFactory } from '@modules/tool/external-tool/testing'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; -import { - schoolExternalToolConfigurationStatusEntityFactory, - schoolExternalToolEntityFactory, - schoolExternalToolFactory, -} from '@modules/tool/school-external-tool/testing'; +import { schoolExternalToolEntityFactory, schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; import { SchoolExternalToolQuery } from '@modules/tool/school-external-tool/uc/dto/school-external-tool.types'; import { Test, TestingModule } from '@nestjs/testing'; import { type SchoolEntity } from '@shared/domain/entity'; @@ -55,15 +51,13 @@ describe(SchoolExternalToolRepo.name, () => { const schoolExternalTool1: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, school, - status: schoolExternalToolConfigurationStatusEntityFactory.build({ isDeactivated: false }), - }); - const schoolExternalTool2: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - status: undefined, + isDeactivated: false, }); + const schoolExternalTool2: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId(); const schoolExternalTool3: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, school, - status: schoolExternalToolConfigurationStatusEntityFactory.build({ isDeactivated: true }), + isDeactivated: true, }); return { externalToolEntity, school, schoolExternalTool1, schoolExternalTool2, schoolExternalTool3 }; diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts index 39f9ce92c82..42aa69db1ca 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts @@ -104,7 +104,7 @@ export class SchoolExternalToolRepo { toolId: entity.tool.id, schoolId: entity.school.id, parameters: ExternalToolRepoMapper.mapCustomParameterEntryEntitiesToDOs(entity.schoolParameters), - status: entity.status, + isDeactivated: entity.isDeactivated, }); } @@ -114,7 +114,7 @@ export class SchoolExternalToolRepo { school: this.em.getReference(SchoolEntity, entityDO.schoolId), tool: this.em.getReference(ExternalToolEntity, entityDO.toolId), schoolParameters: ExternalToolRepoMapper.mapCustomParameterEntryDOsToEntities(entityDO.parameters), - status: entityDO.status, + isDeactivated: entityDO.isDeactivated, }; } } diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.spec.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.spec.ts index 42efc0de412..558d8b68ba3 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.spec.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.spec.ts @@ -51,6 +51,7 @@ describe('SchoolExternalToolScope', () => { describe('when isDeactivated parameter is undefined', () => { it('should return scope without added status to query', () => { scope.byIsDeactivated(undefined); + expect(scope.query).toEqual({}); }); }); @@ -58,14 +59,16 @@ describe('SchoolExternalToolScope', () => { describe('when isDeactivated parameter is false', () => { it('should return scope with added status to query', () => { scope.byIsDeactivated(false); - expect(scope.query).toEqual({ $or: [{ status: { isDeactivated: false } }, { status: undefined }] }); + + expect(scope.query).toEqual({ isDeactivated: false }); }); }); describe('when isDeactivated parameter is true', () => { it('should return scope with added status to query', () => { scope.byIsDeactivated(true); - expect(scope.query).toEqual({ status: { isDeactivated: true } }); + + expect(scope.query).toEqual({ isDeactivated: true }); }); }); }); diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts index 1e5d120d674..1ab1dc0d8b5 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts @@ -1,4 +1,4 @@ -import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import type { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { EntityId } from '@shared/domain/types'; import { Scope } from '@shared/repo/scope'; @@ -18,10 +18,8 @@ export class SchoolExternalToolScope extends Scope { } byIsDeactivated(isDeactivated?: boolean): this { - if (isDeactivated) { - this.addQuery({ status: { isDeactivated } }); - } else if (isDeactivated === false) { - this.addQuery({ $or: [{ status: { isDeactivated } }, { status: undefined }] }); + if (isDeactivated !== undefined) { + this.addQuery({ isDeactivated }); } return this; } diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index be63aa0fe27..dbba0355eee 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -169,5 +169,14 @@ "created_at": { "$date": "2024-06-11T12:51:38.379Z" } + }, + { + "_id": { + "$oid": "667e611e207a39b02c306406" + }, + "name": "Migration20240627134214", + "created_at": { + "$date": "2024-06-28T07:07:10.278Z" + } } ] From a5c764d7298247520aa5482ae4103deea3d8f78b Mon Sep 17 00:00:00 2001 From: Fshmit <122355627+Fshmit@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:01:17 +0200 Subject: [PATCH 19/35] EW-959 upgraded keylocak admin client library to v25.0.1 (#5093) --- package-lock.json | 19 ++++++++----------- package.json | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4761760a394..52c01244aa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@golevelup/nestjs-rabbitmq": "^4.0.0", "@hendt/xml2json": "^1.0.3", "@hpi-schul-cloud/commons": "^1.3.4", - "@keycloak/keycloak-admin-client": "^23.0.6", + "@keycloak/keycloak-admin-client": "^25.0.1", "@lumieducation/h5p-server": "^9.2.0", "@mikro-orm/cli": "^5.6.16", "@mikro-orm/core": "^5.6.16", @@ -3882,13 +3882,13 @@ "license": "MIT" }, "node_modules/@keycloak/keycloak-admin-client": { - "version": "23.0.6", - "license": "Apache-2.0", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-25.0.1.tgz", + "integrity": "sha512-FzJ7OSa6MvBfJbiH/+vlH6Kjz9b74z5eVMPWm4kZmUg1+M4bH3oeSsC2y1Yis72Yrtt62PmlaE2ddaaz+iGi/Q==", "dependencies": { "camelize-ts": "^3.0.0", - "lodash-es": "^4.17.21", "url-join": "^5.0.0", - "url-template": "^3.1.0" + "url-template": "^3.1.1" }, "engines": { "node": ">=18" @@ -16003,10 +16003,6 @@ "version": "4.17.21", "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "license": "MIT" - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "license": "MIT" @@ -23192,8 +23188,9 @@ } }, "node_modules/url-template": { - "version": "3.1.0", - "license": "BSD-3-Clause", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } diff --git a/package.json b/package.json index 27246bf3ed5..c064843247d 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "@golevelup/nestjs-rabbitmq": "^4.0.0", "@hendt/xml2json": "^1.0.3", "@hpi-schul-cloud/commons": "^1.3.4", - "@keycloak/keycloak-admin-client": "^23.0.6", + "@keycloak/keycloak-admin-client": "^25.0.1", "@lumieducation/h5p-server": "^9.2.0", "@mikro-orm/cli": "^5.6.16", "@mikro-orm/core": "^5.6.16", From 61e025012bcb9f719feb73b4016fe2f9c66a2d19 Mon Sep 17 00:00:00 2001 From: mkreuzkam-cap <144103168+mkreuzkam-cap@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:35:39 +0200 Subject: [PATCH 20/35] EW-957: Add erwinidm config variables to admin-api deployment. (#5095) --- .../templates/admin-api-server-configmap.yml.j2 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 b/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 index ce777261dd0..8e0e2e76135 100644 --- a/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 @@ -14,3 +14,9 @@ data: CALENDAR_URI: "{{ CALENDAR_URI }}" ROCKET_CHAT_URI: "{{ ROCKET_CHAT_URI }}" ETHERPAD__PAD_URI: "https://{{ DOMAIN }}/etherpad/p" + FEATURE_IDENTITY_MANAGEMENT_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_ENABLED }}" + FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED }}" + FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED }}" + IDENTITY_MANAGEMENT__URI: "{{ IDENTITY_MANAGEMENT__URI }}" + IDENTITY_MANAGEMENT__TENANT: "{{ IDENTITY_MANAGEMENT__TENANT }}" + IDENTITY_MANAGEMENT__CLIENTID: "{{ IDENTITY_MANAGEMENT__CLIENTID }}" From 7de9c16e4cce0c48f3cb7a64daf52726f1f53f9d Mon Sep 17 00:00:00 2001 From: Igor Richter <93926487+IgorCapCoder@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:06:42 +0200 Subject: [PATCH 21/35] N21-2019 extend metadata response (#5071) --- .../context-external-tool-count-per-context.response.ts | 4 ++++ .../external-tool/controller/api-test/tool.api.spec.ts | 8 ++++++++ .../controller/api-test/tool-school.api.spec.ts | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts index 7e34d995267..548c737c064 100644 --- a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts +++ b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts @@ -7,8 +7,12 @@ export class ContextExternalToolCountPerContextResponse { @ApiProperty() boardElement: number; + @ApiProperty() + mediaBoard: number; + constructor(props: ContextExternalToolCountPerContextResponse) { this.course = props.course; this.boardElement = props.boardElement; + this.mediaBoard = props.mediaBoard; } } diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index 6361294bd08..5e304b9bb60 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -785,6 +785,12 @@ describe('ToolController (API)', () => { contextId: new ObjectId().toHexString(), }); + const mediaBoardTools: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildListWithId(2, { + schoolTool: schoolExternalToolEntitys[1], + contextType: ContextExternalToolType.MEDIA_BOARD, + contextId: new ObjectId().toHexString(), + }); + const board = columnBoardEntityFactory.build(); const externalToolElements = externalToolElementEntityFactory .withParent(board) @@ -799,6 +805,7 @@ describe('ToolController (API)', () => { ...schoolExternalToolEntitys, ...courseTools, ...boardTools, + ...mediaBoardTools, board, ...externalToolElements, ]); @@ -820,6 +827,7 @@ describe('ToolController (API)', () => { contextExternalToolCountPerContext: { course: 1, boardElement: 1, + mediaBoard: 1, }, }); }); 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 b75027fbb60..14337c32f60 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 @@ -556,6 +556,13 @@ describe('ToolSchoolController (API)', () => { } ); + const mediaBoardExternalToolEntitys: ContextExternalToolEntity[] = + contextExternalToolEntityFactory.buildListWithId(2, { + schoolTool: schoolExternalToolEntity, + contextType: ContextExternalToolType.MEDIA_BOARD, + contextId: new ObjectId().toHexString(), + }); + const board = columnBoardEntityFactory.build(); const externalToolElements = externalToolElementEntityFactory.withParent(board).buildList(2, { contextExternalToolId: boardExternalToolEntitys[0].id, @@ -571,6 +578,7 @@ describe('ToolSchoolController (API)', () => { schoolExternalToolEntity, ...courseExternalToolEntitys, ...boardExternalToolEntitys, + ...mediaBoardExternalToolEntitys, board, ...externalToolElements, ]); @@ -591,6 +599,7 @@ describe('ToolSchoolController (API)', () => { contextExternalToolCountPerContext: { course: 1, boardElement: 1, + mediaBoard: 1, }, }); }); From 8389184f71ac5d05495d2d035f76aab13e0f5aa9 Mon Sep 17 00:00:00 2001 From: Martin Schuhmacher <55735359+MartinSchuhmacher@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:40:40 +0200 Subject: [PATCH 22/35] BC-6918 - Adjusting new school year 24/25 to seed data and test (#5104) - adjusting new school year 24/25 to seed data and test --- backup/setup/classes.json | 30 +++++++++++++++++++ backup/setup/schools.json | 22 +++++++------- .../LDAPSyncerConsumer.integration.test.js | 8 ++--- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/backup/setup/classes.json b/backup/setup/classes.json index b53a41a6ff6..c1aecf2afb7 100644 --- a/backup/setup/classes.json +++ b/backup/setup/classes.json @@ -150,6 +150,36 @@ "$date": "2023-07-31T10:01:29.382Z" }, "__v": 0 + }, + { + "_id": { + "$oid": "668c0a7b46c42b79f6034ff0" + }, + "userIds": [ + { + "$oid": "5fa2cccab229544f2c696917" + } + ], + "teacherIds": [ + { + "$oid": "5fa2c71bb229544f2c6966d9" + } + ], + "schoolId": { + "$oid": "5fa2c5ccb229544f2c69666c" + }, + "name": "b", + "gradeLevel": 9, + "year": { + "$oid": "5ebd6dc14a431f75ec9a3e7a" + }, + "createdAt": { + "$date": "2024-07-08T10:00:26.985Z" + }, + "updatedAt": { + "$date": "2024-07-08T10:01:29.382Z" + }, + "__v": 0 }, { "_id": { diff --git a/backup/setup/schools.json b/backup/setup/schools.json index 4681123d100..77b09a9085e 100644 --- a/backup/setup/schools.json +++ b/backup/setup/schools.json @@ -25,7 +25,7 @@ "__v": 0, "fileStorageType": "awsS3", "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "demo", "permissions": { @@ -53,7 +53,7 @@ }, "__v": 0, "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "demo", "permissions": { @@ -89,7 +89,7 @@ }, "__v": 0, "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "demo", "features": [ @@ -131,7 +131,7 @@ "__v": 0, "fileStorageType": "awsS3", "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "expert", "features" : [ @@ -167,7 +167,7 @@ }, "fileStorageType": "awsS3", "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "createdAt": { "$date": "2020-11-04T15:16:28.827Z" @@ -216,7 +216,7 @@ }, "fileStorageType": "awsS3", "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "createdAt": { "$date": "2020-11-04T21:11:14.312Z" @@ -258,7 +258,7 @@ "fileStorageType": "awsS3", "timezone": "America/Belem", "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "createdAt": { "$date": "2020-11-04T21:38:05.110Z" @@ -303,7 +303,7 @@ }, "fileStorageType": "awsS3", "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "createdAt": { "$date": "2020-12-08T16:58:36.527Z" @@ -370,7 +370,7 @@ }, "__v": 0, "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "demo", "features": [ @@ -412,7 +412,7 @@ }, "__v": 0, "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "demo", "features": [ @@ -454,7 +454,7 @@ }, "__v": 0, "currentYear": { - "$oid": "5ebd6dc14a431f75ec9a3e79" + "$oid": "5ebd6dc14a431f75ec9a3e7a" }, "purpose": "demo", "features": [ diff --git a/test/services/sync/strategies/LDAPSyncerConsumer.integration.test.js b/test/services/sync/strategies/LDAPSyncerConsumer.integration.test.js index bcfca245844..8a2ab5cda53 100644 --- a/test/services/sync/strategies/LDAPSyncerConsumer.integration.test.js +++ b/test/services/sync/strategies/LDAPSyncerConsumer.integration.test.js @@ -69,10 +69,10 @@ describe('Ldap Syncer Consumer Integration', () => { it('should create school by the data', async () => { const schoolName = 'test school'; const currentYear = { - _id: '5ebd6dc14a431f75ec9a3e77', - name: '2023/24', - startDate: '2023-08-01T00:00:00.000Z', - endDate: '2024-07-31T00:00:00.000Z', + _id: '5ebd6dc14a431f75ec9a3e7a', + name: '2024/25', + startDate: '2024-08-01T00:00:00.000Z', + endDate: '2025-07-31T00:00:00.000Z', }; const states = await app.service('federalStates').find({ query: { abbreviation: 'NI' } }); const federalStateId = states.data[0]._id; From f8ff58bc9d4c0f9a6c9a512281131eb7abd12052 Mon Sep 17 00:00:00 2001 From: Martin Schuhmacher <55735359+MartinSchuhmacher@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:34:25 +0200 Subject: [PATCH 23/35] BC-6918 - Removing script (#5105) - removing unused script for SJW --- scripts/startNewSchoolYear.js | 60 ----------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 scripts/startNewSchoolYear.js diff --git a/scripts/startNewSchoolYear.js b/scripts/startNewSchoolYear.js deleted file mode 100644 index 526b55e90f0..00000000000 --- a/scripts/startNewSchoolYear.js +++ /dev/null @@ -1,60 +0,0 @@ -const appPromise = require('../src/app'); - -const { info, error } = require('../src/logger'); - -const { yearModel, schoolModel } = require('../src/services/school/model'); -const federalStateModel = require('../src/services/federalState/model'); - -const exceptFederalStateNames = ['']; - -const CURRENT_SCHOOL_YEAR = '2022/23'; -const NEXT_SCHOOL_YEAR = '2023/24'; -const MAINTENANCE_START_DATE = new Date('2023-08-01'); - -appPromise - .then(async () => { - const currentSchoolYearId = await yearModel.findOne({ name: CURRENT_SCHOOL_YEAR }).select('_id').lean().exec(); - const nextSchoolYearId = await yearModel.findOne({ name: NEXT_SCHOOL_YEAR }).select('_id').lean().exec(); - - const federalStates = await federalStateModel - .find({ name: { $nin: exceptFederalStateNames } }) - .select('_id name') - .lean() - .exec(); - const federalStateIds = federalStates.map((state) => state._id); - const federalStateNames = federalStates.map((state) => state.name); - federalStateIds.push(null); - info(`Migrating schools in ${federalStateIds.length} federalstates (${federalStateNames.toString()})`); - - info('Setting up Maintenance mode for LDAP schools'); - const resultLdapSchools = await schoolModel - .updateMany( - { - federalState: { $in: federalStateIds }, - ldapSchoolIdentifier: { $exists: true }, - inMaintenanceSince: { $exists: false }, - currentYear: currentSchoolYearId._id, - }, - { inMaintenanceSince: MAINTENANCE_START_DATE } - ) - .exec(); - info(`LDAP Schools set in Maintenance mode: ${resultLdapSchools.modifiedCount} schools updated`); - - const resultNonLdapSchools = await schoolModel - .updateMany( - { - federalState: { $in: federalStateIds }, - $or: [{ ldapSchoolIdentifier: { $exists: false } }, { ldapSchoolIdentifier: '' }], - currentYear: { $exists: true }, - }, - { currentYear: nextSchoolYearId._id } - ) - .exec(); - info(`Non-LDAP Schools changed year: ${resultNonLdapSchools.modifiedCount} schools updated`); - - return process.exit(0); - }) - .catch((err) => { - error(err); - return process.exit(1); - }); From 7f5640f85e0c68ca1c2c593864f3c58709e0f75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:38:08 +0200 Subject: [PATCH 24/35] N21-1547 Refactor systems module (#5101) --- .../identity-management-oauth.service.ts | 4 +- .../mapper/identity-provider.mapper.spec.ts | 8 +- .../mapper/identity-provider.mapper.ts | 4 +- ...-configuration.service.integration.spec.ts | 13 +- .../keycloak-configuration.service.spec.ts | 28 +-- .../service/keycloak-configuration.service.ts | 21 +- ...-identity-management-oauth.service.spec.ts | 8 +- ...cloak-identity-management-oauth.service.ts | 15 +- .../src/modules/account/account.module.ts | 10 +- .../authentication/authentication.module.ts | 7 +- .../controllers/api-test/login.api.spec.ts | 3 +- .../services/ldap.service.spec.ts | 10 +- .../authentication/services/ldap.service.ts | 8 +- .../strategy/ldap.strategy.spec.ts | 54 ++-- .../authentication/strategy/ldap.strategy.ts | 16 +- .../rules/school-system-options.rule.spec.ts | 3 +- .../domain/rules/school.rule.spec.ts | 4 +- .../domain/rules/system.rule.spec.ts | 3 +- .../fwu-learning-contents-test.module.ts | 7 +- .../fwu-learning-contents.module.ts | 5 +- .../controller/api-test/group.api.spec.ts | 11 +- .../modules/group/repo/group-domain.mapper.ts | 3 +- .../src/modules/group/repo/group.repo.spec.ts | 3 +- .../modules/group/uc/class-group.uc.spec.ts | 23 +- .../src/modules/group/uc/class-group.uc.ts | 20 +- .../src/modules/group/uc/group.uc.spec.ts | 11 +- .../group/uc/mapper/group-uc.mapper.ts | 4 +- .../controller/api-test/school.api.spec.ts | 5 +- .../entity/school-system-options.entity.ts | 2 +- .../repo/school-system-options-repo.mapper.ts | 3 +- .../repo/school-system-options.repo.spec.ts | 3 +- .../repo/school-system-options.repo.ts | 2 +- .../modules/management/seed-data/schools.ts | 11 +- .../modules/management/seed-data/systems.ts | 2 +- .../uc/database-management.uc.spec.ts | 3 +- .../management/uc/database-management.uc.ts | 5 +- .../oauth/controller/oauth-sso.controller.ts | 3 + .../oauth/service/hydra.service.spec.ts | 3 +- .../modules/oauth/service/hydra.service.ts | 7 +- .../oauth/service/oauth.service.spec.ts | 52 ++-- .../modules/oauth/service/oauth.service.ts | 11 +- .../modules/oauth/uc/hydra-oauth.uc.spec.ts | 2 +- .../src/modules/oauth/uc/hydra-oauth.uc.ts | 7 +- .../provisioning-system-input.mapper.spec.ts | 28 ++- .../provisioning-system-input.mapper.ts | 8 +- .../service/provisioning.service.spec.ts | 42 ++-- .../service/provisioning.service.ts | 9 +- .../school/api/test/school-patch.api.spec.ts | 7 +- .../src/modules/school/domain/do/school.ts | 21 +- .../school/domain/event-handler/index.ts | 1 + .../system-deleted.handler.spec.ts | 107 ++++++++ .../event-handler/system-deleted.handler.ts | 24 ++ .../domain/factory/school.factory.spec.ts | 5 +- .../domain/service/school.service.spec.ts | 30 +++ .../school/domain/service/school.service.ts | 19 +- .../mapper/school.entity.mapper.spec.ts | 2 +- .../mikro-orm/mapper/school.entity.mapper.ts | 4 +- .../src/modules/school/school.module.ts | 2 + .../modules/school/testing/school.factory.ts | 3 +- .../controller/api-test/system.api.spec.ts | 3 +- .../controller/dto/oauth-config.response.ts | 15 +- .../controller/dto/public-system-response.ts | 2 +- .../controller/dto/system.filter.params.ts | 20 +- .../src/modules/system/controller/index.ts | 1 + .../mapper/system-response.mapper.ts | 41 ++- .../system/controller/system.controller.ts | 10 +- .../src/modules/system/domain/event/index.ts | 1 + .../domain/event/system-deleted.event.ts | 13 + .../modules/system/domain/factory/index.ts | 1 - .../domain/factory/system.factory.spec.ts | 18 -- .../system/domain/factory/system.factory.ts | 7 - .../server/src/modules/system/domain/index.ts | 3 +- .../modules/system/domain/interface/index.ts | 3 +- .../system/domain/interface/system-query.ts | 5 + .../domain/interface/system.repo.interface.ts | 3 + .../src/modules/system/domain/ldap-config.ts | 26 ++ .../src/modules/system/domain/oidc-config.ts | 28 +++ .../src/modules/system/domain/query/index.ts | 1 - .../system/domain/query/system-query.ts | 5 - .../src/modules/system/domain/system.do.ts | 23 ++ .../server/src/modules/system/entity/index.ts | 7 + .../system}/entity/system.entity.spec.ts | 0 .../system}/entity/system.entity.ts | 4 +- apps/server/src/modules/system/index.ts | 14 +- .../server/src/modules/system/mapper/index.ts | 2 - .../system/mapper/system-oidc.mapper.spec.ts | 76 ------ .../system/mapper/system-oidc.mapper.ts | 31 --- .../system/mapper/system.mapper.spec.ts | 82 ------ .../modules/system/mapper/system.mapper.ts | 42 ---- .../mikro-orm/mapper/system-entity.mapper.ts | 30 ++- .../system/repo/mikro-orm/scope/index.ts | 1 + .../repo/mikro-orm/scope/system.scope.spec.ts | 46 ++++ .../repo/mikro-orm/scope/system.scope.ts | 15 +- .../system/repo/mikro-orm/system.repo.spec.ts | 118 +++++++-- .../system/repo/mikro-orm/system.repo.ts | 19 +- .../src/modules/system/service/dto/index.ts | 3 - .../system/service/dto/oauth-config.dto.ts | 46 ---- .../system/service/dto/oidc-config.dto.ts | 31 --- .../modules/system/service/dto/system.dto.ts | 35 --- .../src/modules/system/service/index.ts | 3 - .../service/legacy-system.service.spec.ts | 238 ------------------ .../system/service/legacy-system.service.ts | 96 ------- .../service/system-oidc.service.spec.ts | 73 ------ .../system/service/system-oidc.service.ts | 26 -- .../system/service/system.service.spec.ts | 97 ++++++- .../modules/system/service/system.service.ts | 25 +- .../src/modules/system/system-api.module.ts | 8 +- .../src/modules/system/system.module.ts | 16 +- .../src/modules/system/uc/system.uc.spec.ts | 236 ++++++++--------- .../server/src/modules/system/uc/system.uc.ts | 50 ++-- .../api-test/import-user.api.spec.ts | 3 +- .../mapper/schulconnex-import-user.mapper.ts | 12 +- ...lconnex-fetch-import-users.service.spec.ts | 17 +- .../schulconnex-fetch-import-users.service.ts | 12 +- .../service/user-import.service.spec.ts | 21 +- .../service/user-import.service.ts | 11 +- .../uc/user-import-fetch.uc.spec.ts | 9 +- .../user-import/uc/user-import-fetch.uc.ts | 5 +- .../user-import/uc/user-import.uc.spec.ts | 23 +- .../modules/user-import/uc/user-import.uc.ts | 13 +- .../modules/user-import/user-import.module.ts | 5 +- .../user-login-migration-rollback.api.spec.ts | 5 +- .../api-test/user-login-migration.api.spec.ts | 3 +- .../user-login-migration.service.spec.ts | 40 ++- .../service/user-login-migration.service.ts | 19 +- .../uc/user-login-migration.uc.spec.ts | 3 +- .../src/shared/domain/entity/all-entities.ts | 8 +- .../entity/external-source.embeddable.ts | 2 +- .../domain/entity/import-user.entity.ts | 2 +- apps/server/src/shared/domain/entity/index.ts | 1 - .../src/shared/domain/entity/school.entity.ts | 2 +- .../entity/user-login-migration.entity.ts | 2 +- apps/server/src/shared/repo/index.ts | 1 - .../legacy-school.repo.integration.spec.ts | 2 +- .../shared/repo/school/legacy-school.repo.ts | 3 +- apps/server/src/shared/repo/scope.ts | 5 +- apps/server/src/shared/repo/system/index.ts | 1 - .../legacy-system.repo.integration.spec.ts | 110 -------- .../shared/repo/system/legacy-system.repo.ts | 39 --- .../src/shared/repo/system/system-scope.ts | 19 -- .../user/user-do.repo.integration.spec.ts | 3 +- .../repo/user/user.repo.integration.spec.ts | 3 +- ...r-login-migration.repo.integration.spec.ts | 5 +- .../user-login-migration.repo.ts | 3 +- .../testing/factory/domainobject/index.ts | 7 +- .../domainobject/system/system.factory.ts | 77 +++++- .../testing/factory/systemEntityFactory.ts | 2 +- 147 files changed, 1257 insertions(+), 1679 deletions(-) create mode 100644 apps/server/src/modules/school/domain/event-handler/index.ts create mode 100644 apps/server/src/modules/school/domain/event-handler/system-deleted.handler.spec.ts create mode 100644 apps/server/src/modules/school/domain/event-handler/system-deleted.handler.ts create mode 100644 apps/server/src/modules/system/controller/index.ts create mode 100644 apps/server/src/modules/system/domain/event/index.ts create mode 100644 apps/server/src/modules/system/domain/event/system-deleted.event.ts delete mode 100644 apps/server/src/modules/system/domain/factory/index.ts delete mode 100644 apps/server/src/modules/system/domain/factory/system.factory.spec.ts delete mode 100644 apps/server/src/modules/system/domain/factory/system.factory.ts create mode 100644 apps/server/src/modules/system/domain/interface/system-query.ts create mode 100644 apps/server/src/modules/system/domain/oidc-config.ts delete mode 100644 apps/server/src/modules/system/domain/query/index.ts delete mode 100644 apps/server/src/modules/system/domain/query/system-query.ts create mode 100644 apps/server/src/modules/system/entity/index.ts rename apps/server/src/{shared/domain => modules/system}/entity/system.entity.spec.ts (100%) rename apps/server/src/{shared/domain => modules/system}/entity/system.entity.ts (97%) delete mode 100644 apps/server/src/modules/system/mapper/index.ts delete mode 100644 apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts delete mode 100644 apps/server/src/modules/system/mapper/system-oidc.mapper.ts delete mode 100644 apps/server/src/modules/system/mapper/system.mapper.spec.ts delete mode 100644 apps/server/src/modules/system/mapper/system.mapper.ts create mode 100644 apps/server/src/modules/system/repo/mikro-orm/scope/index.ts create mode 100644 apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.spec.ts delete mode 100644 apps/server/src/modules/system/service/dto/index.ts delete mode 100644 apps/server/src/modules/system/service/dto/oauth-config.dto.ts delete mode 100644 apps/server/src/modules/system/service/dto/oidc-config.dto.ts delete mode 100644 apps/server/src/modules/system/service/dto/system.dto.ts delete mode 100644 apps/server/src/modules/system/service/legacy-system.service.spec.ts delete mode 100644 apps/server/src/modules/system/service/legacy-system.service.ts delete mode 100644 apps/server/src/modules/system/service/system-oidc.service.spec.ts delete mode 100644 apps/server/src/modules/system/service/system-oidc.service.ts delete mode 100644 apps/server/src/shared/repo/system/index.ts delete mode 100644 apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts delete mode 100644 apps/server/src/shared/repo/system/legacy-system.repo.ts delete mode 100644 apps/server/src/shared/repo/system/system-scope.ts diff --git a/apps/server/src/infra/identity-management/identity-management-oauth.service.ts b/apps/server/src/infra/identity-management/identity-management-oauth.service.ts index 2a486e87c32..d971a0dddd9 100644 --- a/apps/server/src/infra/identity-management/identity-management-oauth.service.ts +++ b/apps/server/src/infra/identity-management/identity-management-oauth.service.ts @@ -1,4 +1,4 @@ -import { OauthConfigDto } from '@modules/system/service/dto'; +import type { OauthConfig } from '@modules/system'; export abstract class IdentityManagementOauthService { /** @@ -6,7 +6,7 @@ export abstract class IdentityManagementOauthService { * @returns the oauth config of the IDM. * @throws an error if the IDM oauth config is not available. */ - abstract getOauthConfig(): Promise; + abstract getOauthConfig(): Promise; /** * Checks if the IDM oauth config is available. diff --git a/apps/server/src/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 index 94ef9c042a4..cbf443d4d0a 100644 --- a/apps/server/src/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 @@ -1,9 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; +import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { OidcConfig } from '@modules/system/domain'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; -import { OidcConfigDto } from '@modules/system/service'; import { OidcIdentityProviderMapper } from './identity-provider.mapper'; describe('OidcIdentityProviderMapper', () => { @@ -32,8 +31,7 @@ describe('OidcIdentityProviderMapper', () => { describe('mapToKeycloakIdentityProvider', () => { const brokerFlowAlias = 'flow'; - const internalRepresentation: OidcConfigDto = { - parentSystemId: new ObjectId(0).toString(), + const internalRepresentation: OidcConfig = { clientId: 'clientId', clientSecret: 'clientSecret', idpHint: 'alias', diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts index a7f9e360074..75bd4ae3794 100644 --- a/apps/server/src/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,12 +1,12 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; -import { OidcConfigDto } from '@modules/system/service'; +import type { OidcConfig } from '@modules/system'; import { Inject } from '@nestjs/common'; export class OidcIdentityProviderMapper { constructor(@Inject(DefaultEncryptionService) private readonly defaultEncryptionService: EncryptionService) {} - public mapToKeycloakIdentityProvider(oidcConfig: OidcConfigDto, flowAlias: string): IdentityProviderRepresentation { + public mapToKeycloakIdentityProvider(oidcConfig: OidcConfig, flowAlias: string): IdentityProviderRepresentation { return { providerId: 'oidc', alias: oidcConfig.idpHint, diff --git a/apps/server/src/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 index bae96a2d119..01d7f066ca3 100644 --- a/apps/server/src/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 @@ -1,12 +1,10 @@ -import { createMock } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import AuthenticationExecutionExportRepresentation from '@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionExportRepresentation'; import AuthenticationFlowRepresentation from '@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation'; -import { LegacySystemService } from '@modules/system'; +import { EntityManager } from '@mikro-orm/mongodb'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySystemRepo } from '@shared/repo'; import { systemEntityFactory } from '@shared/testing/factory'; import { LoggerModule } from '@src/core/logger'; import { v1 } from 'uuid'; @@ -17,14 +15,13 @@ import { KeycloakConfigurationService } from './keycloak-configuration.service'; describe('KeycloakConfigurationService Integration', () => { let module: TestingModule; let keycloak: KeycloakAdminClient; - let systemRepo: LegacySystemRepo; + let em: EntityManager; let keycloakAdministrationService: KeycloakAdministrationService; let keycloakConfigurationService: KeycloakConfigurationService; let isKeycloakAvailable = false; const testRealm = `test-realm-${v1().toString()}`; const flowAlias = 'Direct Broker Flow'; - const systemServiceMock = createMock(); const systems = systemEntityFactory.withOidcConfig().buildList(1); beforeAll(async () => { @@ -38,15 +35,14 @@ describe('KeycloakConfigurationService Integration', () => { validationOptions: { infer: true }, }), ], - providers: [LegacySystemRepo], }).compile(); - systemRepo = module.get(LegacySystemRepo); + em = module.get(EntityManager); keycloakAdministrationService = module.get(KeycloakAdministrationService); keycloakConfigurationService = module.get(KeycloakConfigurationService); isKeycloakAvailable = await keycloakAdministrationService.testKcConnection(); if (isKeycloakAvailable) { keycloak = await keycloakAdministrationService.callKcAdminClient(); - await systemRepo.save(systems); + await em.persistAndFlush(systems); } }); @@ -137,7 +133,6 @@ describe('KeycloakConfigurationService Integration', () => { 'should sync identity providers to keycloak', async () => { if (!isKeycloakAvailable) return; - systemServiceMock.findByType.mockResolvedValueOnce(systems); await keycloakConfigurationService.configureBrokerFlows(); await keycloakConfigurationService.configureIdentityProviders(); diff --git a/apps/server/src/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 index 11bf59c9d8c..7818eda1655 100644 --- a/apps/server/src/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 @@ -7,15 +7,12 @@ import { AuthenticationManagement } from '@keycloak/keycloak-admin-client/lib/re import { Clients } from '@keycloak/keycloak-admin-client/lib/resources/clients'; import { IdentityProviders } from '@keycloak/keycloak-admin-client/lib/resources/identityProviders'; import { Realms } from '@keycloak/keycloak-admin-client/lib/resources/realms'; -import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; -import { SystemOidcService } from '@modules/system/service/system-oidc.service'; +import { SystemService } from '@modules/system'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { SystemEntity } from '@shared/domain/entity'; -import { SystemTypeEnum } from '@shared/domain/types'; -import { systemEntityFactory } from '@shared/testing'; +import { systemFactory } from '@shared/testing'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { v1 } from 'uuid'; @@ -32,7 +29,7 @@ describe('KeycloakConfigurationService Unit', () => { let client: DeepMocked; let service: KeycloakConfigurationService; let configService: DeepMocked; - let systemOidcService: DeepMocked; + let systemService: DeepMocked; let httpServiceMock: DeepMocked; let settings: IKeycloakSettings; @@ -65,14 +62,11 @@ describe('KeycloakConfigurationService Unit', () => { }; }; - const systems: SystemEntity[] = systemEntityFactory - .withOidcConfig() - .buildListWithId(1, { type: SystemTypeEnum.OIDC }); - const oidcSystems = SystemOidcMapper.mapFromEntitiesToDtos(systems); + const systems = systemFactory.withOidcConfig().buildList(1); const idps: IdentityProviderRepresentation[] = [ { providerId: 'oidc', - alias: oidcSystems[0].idpHint, + alias: systems[0].oidcConfig?.idpHint, enabled: true, config: { clientId: 'clientId', @@ -115,8 +109,8 @@ describe('KeycloakConfigurationService Unit', () => { }), }, { - provide: SystemOidcService, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: KeycloakSettings, @@ -142,7 +136,7 @@ describe('KeycloakConfigurationService Unit', () => { service = module.get(KeycloakConfigurationService); configService = module.get(ConfigService); settings = module.get(KeycloakSettings); - systemOidcService = module.get(SystemOidcService); + systemService = module.get(SystemService); httpServiceMock = module.get(HttpService); configService.get.mockImplementation((key: string) => `${key}-value`); @@ -153,7 +147,7 @@ describe('KeycloakConfigurationService Unit', () => { }); beforeEach(() => { - systemOidcService.findAll.mockResolvedValue(oidcSystems); + systemService.find.mockResolvedValue(systems); kcApiClientIdentityProvidersMock.find.mockResolvedValue(idps); kcApiClientIdentityProvidersMock.create.mockResolvedValue({ id: '' }); kcApiClientIdentityProvidersMock.update.mockResolvedValue(); @@ -178,7 +172,7 @@ describe('KeycloakConfigurationService Unit', () => { expect(kcApiClientIdentityProvidersMock.update).toBeCalledTimes(1); }); it('should delete a new configuration in Keycloak', async () => { - systemOidcService.findAll.mockResolvedValue([]); + systemService.find.mockResolvedValue([]); const result = await service.configureIdentityProviders(); expect(result).toBe(1); @@ -210,7 +204,7 @@ describe('KeycloakConfigurationService Unit', () => { kcApiClientMock.find.mockResolvedValue([]); kcApiClientMock.create.mockResolvedValue({ id: 'new_client_id' }); kcApiClientMock.generateNewClientSecret.mockResolvedValue({ type: 'secret', value: 'generated_client_secret' }); - systemOidcService.findAll.mockResolvedValue([]); + systemService.find.mockResolvedValue([]); const response = { data: { token_endpoint: 'tokenEndpoint', diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts index 89389a5318d..73e3e3be837 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts @@ -5,8 +5,8 @@ import IdentityProviderMapperRepresentation from '@keycloak/keycloak-admin-clien import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import ProtocolMapperRepresentation from '@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation'; import { ServerConfig } from '@modules/server/server.config'; -import { OidcConfigDto } from '@modules/system/service'; -import { SystemOidcService } from '@modules/system/service/system-oidc.service'; +import { OidcConfig, SystemType } from '@modules/system/domain'; +import { SystemService } from '@modules/system/service'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; @@ -28,7 +28,7 @@ export class KeycloakConfigurationService { private readonly kcAdmin: KeycloakAdministrationService, private readonly configService: ConfigService, private readonly oidcIdentityProviderMapper: OidcIdentityProviderMapper, - private readonly systemOidcService: SystemOidcService + private readonly systemService: SystemService ) {} public async configureBrokerFlows(): Promise { @@ -129,7 +129,10 @@ export class KeycloakConfigurationService { let count = 0; const kc = await this.kcAdmin.callKcAdminClient(); const oldConfigs = await kc.identityProviders.find(); - const newConfigs = await this.systemOidcService.findAll(); + const oidcSystems = await this.systemService.find({ types: [SystemType.OIDC] }); + const newConfigs = oidcSystems + .map((entity) => entity.oidcConfig) + .filter((entity): entity is OidcConfig => entity !== undefined); const configureActions = this.selectConfigureAction(newConfigs, oldConfigs); // eslint-disable-next-line no-restricted-syntax for (const configureAction of configureActions) { @@ -188,10 +191,10 @@ export class KeycloakConfigurationService { * @param oldConfigs * @returns */ - private selectConfigureAction(newConfigs: OidcConfigDto[], oldConfigs: IdentityProviderRepresentation[]) { + private selectConfigureAction(newConfigs: OidcConfig[], oldConfigs: IdentityProviderRepresentation[]) { const result = [] as ( - | { action: ConfigureAction.CREATE; config: OidcConfigDto } - | { action: ConfigureAction.UPDATE; config: OidcConfigDto } + | { action: ConfigureAction.CREATE; config: OidcConfig } + | { action: ConfigureAction.UPDATE; config: OidcConfig } | { action: ConfigureAction.DELETE; alias: string } )[]; // updating or creating configs @@ -211,7 +214,7 @@ export class KeycloakConfigurationService { return result; } - private async createIdentityProvider(oidcConfig: OidcConfigDto): Promise { + private async createIdentityProvider(oidcConfig: OidcConfig): Promise { const kc = await this.kcAdmin.callKcAdminClient(); if (oidcConfig && oidcConfig?.idpHint) { await kc.identityProviders.create( @@ -221,7 +224,7 @@ export class KeycloakConfigurationService { } } - private async updateIdentityProvider(oidcConfig: OidcConfigDto): Promise { + private async updateIdentityProvider(oidcConfig: OidcConfig): Promise { const kc = await this.kcAdmin.callKcAdminClient(); if (oidcConfig && oidcConfig?.idpHint) { await kc.identityProviders.update( diff --git a/apps/server/src/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 index 8bd354ff379..72f01583aa2 100644 --- a/apps/server/src/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 @@ -14,7 +14,6 @@ describe('KeycloakIdentityManagementService', () => { let kcIdmOauthService: KeycloakIdentityManagementOauthService; let kcAdminServiceMock: DeepMocked; let httpServiceMock: DeepMocked; - let configServiceMock: DeepMocked; let oAuthEncryptionService: DeepMocked; const clientId = 'TheClientId'; @@ -46,7 +45,6 @@ describe('KeycloakIdentityManagementService', () => { kcIdmOauthService = module.get(KeycloakIdentityManagementOauthService); kcAdminServiceMock = module.get(KeycloakAdministrationService); httpServiceMock = module.get(HttpService); - configServiceMock = module.get(ConfigService); }); afterEach(() => { @@ -57,7 +55,6 @@ describe('KeycloakIdentityManagementService', () => { const setupOauthConfigurationReturn = () => { oAuthEncryptionService.encrypt.mockImplementation((value: string) => `${value}_enc`); oAuthEncryptionService.decrypt.mockImplementation((value: string) => value.substring(0, -4)); - configServiceMock.get.mockReturnValue('testdomain'); kcAdminServiceMock.callKcAdminClient.mockResolvedValue({} as KeycloakAdminClient); kcAdminServiceMock.getClientId.mockReturnValueOnce(clientId); kcAdminServiceMock.getClientSecret.mockResolvedValueOnce(clientSecret); @@ -109,7 +106,7 @@ describe('KeycloakIdentityManagementService', () => { const ret = await kcIdmOauthService.getOauthConfig(); - expect(ret.redirectUri).toBe('https://testdomain/api/v3/sso/oauth/'); + expect(ret.redirectUri).toBe(''); }); it('should return the keycloak OAuth configuration from well-known', async () => { @@ -128,7 +125,6 @@ describe('KeycloakIdentityManagementService', () => { describe('when localhost is set as SC DOMAIN', () => { const setup = () => { setupOauthConfigurationReturn(); - configServiceMock.get.mockReturnValue('localhost'); }; it('should return the keycloak OAuth redirect URL for local development', async () => { @@ -136,7 +132,7 @@ describe('KeycloakIdentityManagementService', () => { const ret = await kcIdmOauthService.getOauthConfig(); - expect(ret.redirectUri).toBe('http://localhost:3030/api/v3/sso/oauth/'); + expect(ret.redirectUri).toBe(''); }); }); diff --git a/apps/server/src/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 index b6458640c75..7ee1ce2eb44 100644 --- a/apps/server/src/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,8 +1,7 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; -import { OauthConfigDto } from '@modules/system/service/dto'; +import { OauthConfig } from '@modules/system/domain'; import { HttpService } from '@nestjs/axios'; import { Inject, Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import qs from 'qs'; import { lastValueFrom } from 'rxjs'; import { IdentityManagementOauthService } from '../../identity-management-oauth.service'; @@ -10,31 +9,27 @@ import { KeycloakAdministrationService } from '../../keycloak-administration/ser @Injectable() export class KeycloakIdentityManagementOauthService extends IdentityManagementOauthService { - private _oauthConfigCache: OauthConfigDto | undefined; + private _oauthConfigCache: OauthConfig | undefined; constructor( private readonly kcAdminService: KeycloakAdministrationService, - private readonly configService: ConfigService, private readonly httpService: HttpService, @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService ) { super(); } - async getOauthConfig(): Promise { + async getOauthConfig(): Promise { if (this._oauthConfigCache) { return this._oauthConfigCache; } const wellKnownUrl = this.kcAdminService.getWellKnownUrl(); const response = (await lastValueFrom(this.httpService.get>(wellKnownUrl))).data; - const scDomain = this.configService.get('SC_DOMAIN') || ''; - const redirectUri = - scDomain === 'localhost' ? 'http://localhost:3030/api/v3/sso/oauth/' : `https://${scDomain}/api/v3/sso/oauth/`; - this._oauthConfigCache = new OauthConfigDto({ + this._oauthConfigCache = new OauthConfig({ clientId: this.kcAdminService.getClientId(), clientSecret: this.oAuthEncryptionService.encrypt(await this.kcAdminService.getClientSecret()), provider: 'oauth', - redirectUri, + redirectUri: '', responseType: 'code', grantType: 'authorization_code', scope: 'openid profile email', diff --git a/apps/server/src/modules/account/account.module.ts b/apps/server/src/modules/account/account.module.ts index 0f77a40fafd..4acde7473fb 100644 --- a/apps/server/src/modules/account/account.module.ts +++ b/apps/server/src/modules/account/account.module.ts @@ -1,16 +1,17 @@ import { IdentityManagementModule } from '@infra/identity-management'; +import { SystemModule } from '@modules/system'; import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { LegacySystemRepo, UserRepo } from '@shared/repo'; import { CqrsModule } from '@nestjs/cqrs'; +import { UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger/logger.module'; import { AccountConfig } from './account-config'; -import { AccountRepo } from './repo/micro-orm/account.repo'; -import { AccountIdmToDoMapper, AccountIdmToDoMapperDb, AccountIdmToDoMapperIdm } from './repo/micro-orm/mapper'; import { AccountServiceDb } from './domain/services/account-db.service'; import { AccountServiceIdm } from './domain/services/account-idm.service'; import { AccountService } from './domain/services/account.service'; +import { AccountRepo } from './repo/micro-orm/account.repo'; +import { AccountIdmToDoMapper, AccountIdmToDoMapperDb, AccountIdmToDoMapperIdm } from './repo/micro-orm/mapper'; function accountIdmToDtoMapperFactory(configService: ConfigService): AccountIdmToDoMapper { if (configService.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') === true) { @@ -20,10 +21,9 @@ function accountIdmToDtoMapperFactory(configService: ConfigService { describe('checkLdapCredentials', () => { describe('when credentials are correct', () => { it('should login successfully', async () => { - const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); + const system: System = systemFactory.withLdapConfig().build(); await expect( ldapService.checkLdapCredentials(system, 'connectSucceeds', 'mockPassword') ).resolves.not.toThrow(); @@ -68,7 +68,7 @@ describe('LdapService', () => { describe('when no ldap config is provided', () => { it('should throw error', async () => { - const system: SystemEntity = systemEntityFactory.buildWithId(); + const system: System = systemFactory.build(); await expect(ldapService.checkLdapCredentials(system, 'mockUsername', 'mockPassword')).rejects.toThrow( new Error(`no LDAP config found in system ${system.id}`) ); @@ -77,7 +77,7 @@ describe('LdapService', () => { describe('when user is not authorized', () => { it('should throw UserCouldNotAuthenticateLoggableException', async () => { - const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); + const system: System = systemFactory.withLdapConfig().build(); await expect(ldapService.checkLdapCredentials(system, 'mockUsername', 'mockPassword')).rejects.toThrow( LdapUserCouldNotBeAuthenticatedLoggableException ); diff --git a/apps/server/src/modules/authentication/services/ldap.service.ts b/apps/server/src/modules/authentication/services/ldap.service.ts index 3436d4e76e0..fc56ad12325 100644 --- a/apps/server/src/modules/authentication/services/ldap.service.ts +++ b/apps/server/src/modules/authentication/services/ldap.service.ts @@ -1,9 +1,9 @@ +import type { System } from '@modules/system'; import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { SystemEntity } from '@shared/domain/entity'; import { Logger } from '@src/core/logger'; import { Client, createClient } from 'ldapjs'; import { LdapConnectionError } from '../errors/ldap-connection.error'; -import { UserAuthenticatedLoggable, LdapUserCouldNotBeAuthenticatedLoggableException } from '../loggable'; +import { LdapUserCouldNotBeAuthenticatedLoggableException, UserAuthenticatedLoggable } from '../loggable'; @Injectable() export class LdapService { @@ -11,7 +11,7 @@ export class LdapService { this.logger.setContext(LdapService.name); } - async checkLdapCredentials(system: SystemEntity, username: string, password: string): Promise { + async checkLdapCredentials(system: System, username: string, password: string): Promise { const connection = await this.connect(system, username, password); if (connection.connected) { connection.unbind(); @@ -20,7 +20,7 @@ export class LdapService { throw new UnauthorizedException('User could not authenticate'); } - private connect(system: SystemEntity, username: string, password: string): Promise { + private connect(system: System, username: string, password: string): Promise { const { ldapConfig } = system; if (!ldapConfig) { throw Error(`no LDAP config found in system ${system.id}`); 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 b75653ee7ff..0e537c2ef56 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -1,20 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Account } from '@modules/account'; +import { System, SystemService } from '@modules/system'; import { UnauthorizedException } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { SystemEntity, User } from '@shared/domain/entity'; +import { User } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; -import { LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; - -import { - legacySchoolDoFactory, - schoolEntityFactory, - setupEntities, - systemEntityFactory, - userFactory, -} from '@shared/testing'; +import { LegacySchoolRepo, UserRepo } from '@shared/repo'; +import { legacySchoolDoFactory, schoolEntityFactory, setupEntities, systemFactory, userFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { accountDoFactory, defaultTestPassword, defaultTestPasswordHash } from '@src/modules/account/testing'; import { LdapAuthorizationBodyParams } from '../controllers/dto'; @@ -31,7 +25,7 @@ describe('LdapStrategy', () => { let schoolRepoMock: DeepMocked; let authenticationServiceMock: DeepMocked; let ldapServiceMock: DeepMocked; - let systemRepo: DeepMocked; + let systemService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -57,8 +51,8 @@ describe('LdapStrategy', () => { useValue: createMock(), }, { - provide: LegacySystemRepo, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: Logger, @@ -72,7 +66,7 @@ describe('LdapStrategy', () => { schoolRepoMock = module.get(LegacySchoolRepo); userRepoMock = module.get(UserRepo); ldapServiceMock = module.get(LdapService); - systemRepo = module.get(LegacySystemRepo); + systemService = module.get(SystemService); }); afterAll(async () => { @@ -88,7 +82,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: System = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).build(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: undefined }); @@ -110,7 +104,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -135,7 +129,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: System = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).build(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -157,7 +151,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -182,7 +176,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: System = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).build(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -204,7 +198,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -229,7 +223,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: System = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).build(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -251,7 +245,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -276,7 +270,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: System = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).build(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -298,7 +292,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -328,7 +322,7 @@ describe('LdapStrategy', () => { const error = new Error('error'); const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: System = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).build(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -350,7 +344,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -383,7 +377,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); + const system: System = systemFactory.withLdapConfig().build(); const user: User = userFactory .withRoleByName(RoleName.STUDENT) @@ -410,7 +404,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockResolvedValue(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); authenticationServiceMock.normalizePassword.mockReturnValue(defaultTestPassword); @@ -446,7 +440,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); + const system: System = systemFactory.withLdapConfig().build(); const user: User = userFactory .withRoleByName(RoleName.STUDENT) @@ -473,7 +467,7 @@ describe('LdapStrategy', () => { }, }; - systemRepo.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); authenticationServiceMock.loadAccount.mockRejectedValueOnce(new UnauthorizedException()); authenticationServiceMock.loadAccount.mockResolvedValueOnce(account); authenticationServiceMock.normalizeUsername.mockReturnValue(username); diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts index d732d39d25c..e922b90c13e 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts @@ -1,9 +1,10 @@ import { Account } from '@modules/account'; +import { System, SystemService } from '@modules/system'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { SystemEntity, User } from '@shared/domain/entity'; -import { LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; +import { User } from '@shared/domain/entity'; +import { LegacySchoolRepo, UserRepo } from '@shared/repo'; import { ErrorLoggable } from '@src/core/error/loggable/error.loggable'; import { Logger } from '@src/core/logger'; import { Strategy } from 'passport-custom'; @@ -16,7 +17,7 @@ import { LdapService } from '../services/ldap.service'; @Injectable() export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { constructor( - private readonly systemRepo: LegacySystemRepo, + private readonly systemService: SystemService, private readonly schoolRepo: LegacySchoolRepo, private readonly ldapService: LdapService, private readonly authenticationService: AuthenticationService, @@ -29,7 +30,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { async validate(request: { body: LdapAuthorizationBodyParams }): Promise { const { username, password, systemId, schoolId } = this.extractParamsFromRequest(request); - const system: SystemEntity = await this.systemRepo.findById(systemId); + const system: System = await this.systemService.findByIdOrFail(systemId); const school: LegacySchoolDo = await this.schoolRepo.findById(schoolId); if (!school.systems || !school.systems.includes(systemId)) { @@ -70,12 +71,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { return value; } - private async checkCredentials( - account: Account, - system: SystemEntity, - ldapDn: string, - password: string - ): Promise { + private async checkCredentials(account: Account, system: System, ldapDn: string, password: string): Promise { try { await this.ldapService.checkLdapCredentials(system, ldapDn, password); } catch (error) { diff --git a/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts index 0d937c165a2..1c7e93c3ad4 100644 --- a/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts @@ -1,8 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { SchoolSystemOptions } from '@modules/legacy-school'; +import { SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { schoolEntityFactory, diff --git a/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts index 07758a92d01..9b1ac7446c6 100644 --- a/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { schoolFactory } from '@modules/school/testing/school.factory'; import { Test, TestingModule } from '@nestjs/testing'; -import { setupEntities, userFactory } from '@shared/testing'; +import { schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { AuthorizationContextBuilder } from '../mapper'; import { AuthorizationHelper } from '../service/authorization.helper'; import { SchoolRule } from './school.rule'; @@ -23,7 +23,7 @@ describe('SchoolRule', () => { const setupSchoolAndUser = () => { const school = schoolFactory.build(); - const user = userFactory.build({ school }); + const user = userFactory.build({ school: schoolEntityFactory.buildWithId(undefined, school.id) }); return { school, user }; }; diff --git a/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts index 8fb4d0173ba..04390c64e48 100644 --- a/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts @@ -1,7 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { System } from '@modules/system'; +import { SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { schoolEntityFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; import { AuthorizationContextBuilder } from '../mapper'; 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 612d8a183f8..a1a6671358d 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,19 +2,20 @@ 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 { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { AuthenticationModule } from '@modules/authentication/authentication.module'; import { AuthorizationModule } from '@modules/authorization'; +import { SystemEntity } from '@modules/system/entity'; import { HttpModule } from '@nestjs/axios'; import { DynamicModule, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { Role, SchoolEntity, SchoolYearEntity, SystemEntity, User } from '@shared/domain/entity'; +import { Role, SchoolEntity, SchoolYearEntity, User } from '@shared/domain/entity'; import { createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { FwuLearningContentsUc } from './uc/fwu-learning-contents.uc'; +import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { FwuLearningContentsController } from './controller/fwu-learning-contents.controller'; import { config, s3Config } from './fwu-learning-contents.config'; +import { FwuLearningContentsUc } from './uc/fwu-learning-contents.uc'; const imports = [ MongoMemoryDatabaseModule.forRoot({ 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 36e82ef130b..acf829e15d3 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 @@ -3,11 +3,12 @@ import { S3ClientModule } from '@infra/s3-client'; import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { AuthorizationModule } from '@modules/authorization'; +import { SystemEntity } from '@modules/system/entity'; import { HttpModule } from '@nestjs/axios'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { Role, SchoolEntity, SchoolYearEntity, SystemEntity, User } from '@shared/domain/entity'; -import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; +import { Role, SchoolEntity, SchoolYearEntity, User } from '@shared/domain/entity'; +import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; diff --git a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts index 75c71ffad15..4bd62ffcf36 100644 --- a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts +++ b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts @@ -4,14 +4,7 @@ import { classEntityFactory } from '@modules/class/entity/testing'; import { serverConfig, ServerConfig, ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { - Course as CourseEntity, - Role, - SchoolEntity, - SchoolYearEntity, - SystemEntity, - User, -} from '@shared/domain/entity'; +import { Course as CourseEntity, Role, SchoolEntity, SchoolYearEntity, User } from '@shared/domain/entity'; import { RoleName, SortOrder } from '@shared/domain/interface'; import { courseFactory as courseEntityFactory, @@ -62,7 +55,7 @@ describe('Group (API)', () => { const teacherRole: Role = roleFactory.buildWithId({ name: RoleName.TEACHER }); const teacherUser: User = userFactory.buildWithId({ school, roles: [teacherRole] }); - const system: SystemEntity = systemEntityFactory.buildWithId(); + const system = systemEntityFactory.buildWithId(); const clazz: ClassEntity = classEntityFactory.buildWithId({ name: 'Group A', schoolId: school._id, diff --git a/apps/server/src/modules/group/repo/group-domain.mapper.ts b/apps/server/src/modules/group/repo/group-domain.mapper.ts index 60adfbaec95..c63c83bbab6 100644 --- a/apps/server/src/modules/group/repo/group-domain.mapper.ts +++ b/apps/server/src/modules/group/repo/group-domain.mapper.ts @@ -1,7 +1,8 @@ import { EntityData } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { ExternalSource } from '@shared/domain/domainobject'; -import { ExternalSourceEmbeddable, Role, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ExternalSourceEmbeddable, Role, SchoolEntity, User } from '@shared/domain/entity'; import { Group, GroupProps, GroupTypes, GroupUser } from '../domain'; import { GroupEntity, GroupEntityTypes, GroupUserEmbeddable, GroupValidPeriodEmbeddable } from '../entity'; 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 f42f18bce33..6d5353ef171 100644 --- a/apps/server/src/modules/group/repo/group.repo.spec.ts +++ b/apps/server/src/modules/group/repo/group.repo.spec.ts @@ -1,8 +1,9 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { type SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { ExternalSource, Page } from '@shared/domain/domainobject'; -import { Course as CourseEntity, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { Course as CourseEntity, SchoolEntity, User } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { diff --git a/apps/server/src/modules/group/uc/class-group.uc.spec.ts b/apps/server/src/modules/group/uc/class-group.uc.spec.ts index 4e3f0d0ceee..1a9bb2c0abf 100644 --- a/apps/server/src/modules/group/uc/class-group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/class-group.uc.spec.ts @@ -14,7 +14,7 @@ import { RoleService } from '@modules/role'; import { RoleDto } from '@modules/role/service/dto/role.dto'; import { School, SchoolService } from '@modules/school/domain'; import { schoolFactory } from '@modules/school/testing'; -import { LegacySystemService, SystemDto } from '@modules/system'; +import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -27,6 +27,7 @@ import { roleDtoFactory, schoolYearFactory, setupEntities, + systemFactory, UserAndAccountTestFactory, userDoFactory, userFactory, @@ -46,7 +47,7 @@ describe('ClassGroupUc', () => { let userService: DeepMocked; let roleService: DeepMocked; let classService: DeepMocked; - let systemService: DeepMocked; + let systemService: DeepMocked; let schoolService: DeepMocked; let authorizationService: DeepMocked; let schoolYearService: DeepMocked; @@ -74,8 +75,8 @@ describe('ClassGroupUc', () => { useValue: createMock(), }, { - provide: LegacySystemService, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: SchoolService, @@ -105,7 +106,7 @@ describe('ClassGroupUc', () => { userService = module.get(UserService); roleService = module.get(RoleService); classService = module.get(ClassService); - systemService = module.get(LegacySystemService); + systemService = module.get(SystemService); schoolService = module.get(SchoolService); authorizationService = module.get(AuthorizationService); schoolYearService = module.get(SchoolYearService); @@ -196,10 +197,8 @@ describe('ClassGroupUc', () => { year: undefined, }); - const system: SystemDto = new SystemDto({ - id: new ObjectId().toHexString(), + const system: System = systemFactory.withOauthConfig().build({ displayName: 'External System', - type: 'oauth2', }); const group: Group = groupFactory.build({ name: 'B', @@ -611,10 +610,8 @@ describe('ClassGroupUc', () => { source: 'LDAP', year: schoolYear.id, }); - const system: SystemDto = new SystemDto({ - id: new ObjectId().toHexString(), + const system: System = systemFactory.withOauthConfig().build({ displayName: 'External System', - type: 'oauth2', }); const group: Group = groupFactory.build({ name: 'B', @@ -902,10 +899,8 @@ describe('ClassGroupUc', () => { source: 'LDAP', year: schoolYear.id, }); - const system: SystemDto = new SystemDto({ - id: new ObjectId().toHexString(), + const system: System = systemFactory.withOauthConfig().build({ displayName: 'External System', - type: 'oauth2', }); const group: Group = groupFactory.build({ name: 'B', diff --git a/apps/server/src/modules/group/uc/class-group.uc.ts b/apps/server/src/modules/group/uc/class-group.uc.ts index 5f7843c3885..767591735fe 100644 --- a/apps/server/src/modules/group/uc/class-group.uc.ts +++ b/apps/server/src/modules/group/uc/class-group.uc.ts @@ -13,7 +13,7 @@ import { Page, UserDO } from '@shared/domain/domainobject'; import { SchoolYearEntity, User } from '@shared/domain/entity'; import { Pagination, Permission, SortOrder } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { LegacySystemService, SystemDto } from '@src/modules/system'; +import { System, SystemService } from '@src/modules/system'; import { ClassRequestContext, SchoolYearQueryType } from '../controller/dto/interface'; import { Group, GroupFilter } from '../domain'; import { UnknownQueryTypeLoggableException } from '../loggable'; @@ -26,7 +26,7 @@ export class ClassGroupUc { constructor( private readonly groupService: GroupService, private readonly classService: ClassService, - private readonly systemService: LegacySystemService, + private readonly systemService: SystemService, private readonly schoolService: SchoolService, private readonly authorizationService: AuthorizationService, private readonly schoolYearService: SchoolYearService, @@ -236,7 +236,7 @@ export class ClassGroupUc { } private async getClassInfosFromGroups(groups: Group[]): Promise { - const systemMap: Map = await this.findSystemNamesForGroups(groups); + const systemMap: Map = await this.findSystemNamesForGroups(groups); const classInfosFromGroups: ClassInfoDto[] = await Promise.all( groups.map(async (group: Group): Promise => this.getClassInfoFromGroup(group, systemMap)) @@ -245,8 +245,8 @@ export class ClassGroupUc { return classInfosFromGroups; } - private async getClassInfoFromGroup(group: Group, systemMap: Map): Promise { - let system: SystemDto | undefined; + private async getClassInfoFromGroup(group: Group, systemMap: Map): Promise { + let system: System | undefined; if (group.externalSource) { system = systemMap.get(group.externalSource.systemId); } @@ -268,20 +268,22 @@ export class ClassGroupUc { return mapped; } - private async findSystemNamesForGroups(groups: Group[]): Promise> { + private async findSystemNamesForGroups(groups: Group[]): Promise> { const systemIds: EntityId[] = groups .map((group: Group): string | undefined => group.externalSource?.systemId) .filter((systemId: string | undefined): systemId is EntityId => systemId !== undefined); const uniqueSystemIds: EntityId[] = Array.from(new Set(systemIds)); - const systems: Map = new Map(); + const systems: Map = new Map(); await Promise.all( uniqueSystemIds.map(async (systemId: string): Promise => { - const system: SystemDto = await this.systemService.findById(systemId); + const system: System | null = await this.systemService.findById(systemId); - systems.set(systemId, system); + if (system) { + systems.set(systemId, system); + } }) ); diff --git a/apps/server/src/modules/group/uc/group.uc.spec.ts b/apps/server/src/modules/group/uc/group.uc.spec.ts index cded865cb00..c03258d21ce 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -17,6 +17,7 @@ import { groupFactory, roleDtoFactory, roleFactory, + schoolEntityFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, @@ -327,7 +328,10 @@ describe('GroupUc', () => { const school: School = schoolFactory.build(); const otherSchool: School = schoolFactory.build(); const roles: Role = roleFactory.build({ permissions: [Permission.GROUP_FULL_ADMIN, Permission.GROUP_VIEW] }); - const user: User = userFactory.buildWithId({ roles: [roles], school }); + const user: User = userFactory.buildWithId({ + roles: [roles], + school: schoolEntityFactory.buildWithId(undefined, school.id), + }); const groupInSchool: Group = groupFactory.build({ organizationId: school.id }); const availableGroupInSchool: Group = groupFactory.build({ organizationId: school.id }); @@ -493,7 +497,10 @@ describe('GroupUc', () => { const setup = () => { const school: School = schoolFactory.build(); const roles: Role = roleFactory.build({ permissions: [Permission.GROUP_VIEW] }); - const user: User = userFactory.buildWithId({ roles: [roles], school }); + const user: User = userFactory.buildWithId({ + roles: [roles], + school: schoolEntityFactory.buildWithId(undefined, school.id), + }); const teachersGroup: Group = groupFactory.build({ organizationId: school.id, diff --git a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts index 9dee917acdd..d0da50c17eb 100644 --- a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts +++ b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts @@ -1,5 +1,5 @@ import { Class } from '@modules/class/domain'; -import { SystemDto } from '@modules/system'; +import { System } from '@modules/system'; import { UserDO } from '@shared/domain/domainobject'; import { SchoolYearEntity } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; @@ -13,7 +13,7 @@ export class GroupUcMapper { group: Group, resolvedUsers: ResolvedGroupUser[], synchronizedCourses: Course[], - system?: SystemDto + system?: System ): ClassInfoDto { const mapped: ClassInfoDto = new ClassInfoDto({ id: group.id, diff --git a/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts b/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts index af734110f96..42eebe94443 100644 --- a/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts +++ b/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts @@ -1,17 +1,18 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; +import { SystemEntity } from '@modules/system/entity'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { cleanupCollections, + schoolEntityFactory, schoolSystemOptionsEntityFactory, systemEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { schoolEntityFactory } from '@shared/testing/factory/school-entity.factory'; import { SchoolSystemOptionsEntity } from '../../entity'; import { ProvisioningOptionsInterface } from '../../interface'; import { SchulConneXProvisioningOptionsResponse } from '../dto'; diff --git a/apps/server/src/modules/legacy-school/entity/school-system-options.entity.ts b/apps/server/src/modules/legacy-school/entity/school-system-options.entity.ts index d38ba325df5..b33e370cca3 100644 --- a/apps/server/src/modules/legacy-school/entity/school-system-options.entity.ts +++ b/apps/server/src/modules/legacy-school/entity/school-system-options.entity.ts @@ -1,7 +1,7 @@ import { Embedded, Entity, ManyToOne, Unique } from '@mikro-orm/core'; +import { SystemEntity } from '@modules/system/entity'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { SchoolEntity } from '@shared/domain/entity/school.entity'; -import { SystemEntity } from '@shared/domain/entity/system.entity'; import { EntityId } from '@shared/domain/types'; import { ProvisioningOptionsInterface } from '../interface'; import { ProvisioningOptionsEntity } from './provisioning-options.entity'; diff --git a/apps/server/src/modules/legacy-school/repo/school-system-options-repo.mapper.ts b/apps/server/src/modules/legacy-school/repo/school-system-options-repo.mapper.ts index 44ca21bb878..4172d65261d 100644 --- a/apps/server/src/modules/legacy-school/repo/school-system-options-repo.mapper.ts +++ b/apps/server/src/modules/legacy-school/repo/school-system-options-repo.mapper.ts @@ -1,5 +1,6 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SystemEntity } from '@modules/system/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { AnyProvisioningOptions, SchoolSystemOptions, SchoolSystemOptionsProps } from '../domain'; import { SchoolSystemOptionsEntity, SchoolSystemOptionsEntityProps } from '../entity'; diff --git a/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts b/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts index a268d279bfb..618b56a4773 100644 --- a/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts +++ b/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts @@ -1,7 +1,8 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { schoolEntityFactory, diff --git a/apps/server/src/modules/legacy-school/repo/school-system-options.repo.ts b/apps/server/src/modules/legacy-school/repo/school-system-options.repo.ts index 16c96463c74..3d083e7daec 100644 --- a/apps/server/src/modules/legacy-school/repo/school-system-options.repo.ts +++ b/apps/server/src/modules/legacy-school/repo/school-system-options.repo.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { Injectable } from '@nestjs/common'; -import { SystemEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { EntityId } from '@shared/domain/types'; import { diff --git a/apps/server/src/modules/management/seed-data/schools.ts b/apps/server/src/modules/management/seed-data/schools.ts index 42f74aa90ac..3a6e6ce4bb6 100644 --- a/apps/server/src/modules/management/seed-data/schools.ts +++ b/apps/server/src/modules/management/seed-data/schools.ts @@ -1,16 +1,11 @@ /* eslint-disable @typescript-eslint/dot-notation */ -import { - FederalStateEntity, - SchoolProperties, - SchoolRoles, - SchoolYearEntity, - SystemEntity, -} from '@shared/domain/entity'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; +import { FederalStateEntity, SchoolProperties, SchoolRoles, SchoolYearEntity } from '@shared/domain/entity'; import { LanguageType } from '@shared/domain/interface'; import { SchoolFeature, SchoolPurpose } from '@shared/domain/types'; import { federalStateFactory, schoolEntityFactory } from '@shared/testing'; import { FileStorageType } from '@src/modules/school/domain/type/file-storage-type.enum'; -import { ObjectId } from '@mikro-orm/mongodb'; import { DeepPartial } from 'fishery'; import { EFederalState } from './federalstates'; import { SeedSchoolYearEnum } from './schoolyears'; diff --git a/apps/server/src/modules/management/seed-data/systems.ts b/apps/server/src/modules/management/seed-data/systems.ts index 15b4e6a9306..1a9c531d870 100644 --- a/apps/server/src/modules/management/seed-data/systems.ts +++ b/apps/server/src/modules/management/seed-data/systems.ts @@ -1,5 +1,5 @@ /* eslint-disable no-template-curly-in-string */ -import { SystemEntityProps } from '@shared/domain/entity'; +import { SystemEntityProps } from '@modules/system/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { systemEntityFactory } from '@shared/testing'; import { DeepPartial } from 'fishery'; 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 01f4b6955b5..3c627a6ff7e 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,9 +4,10 @@ import { DatabaseManagementService } from '@infra/database'; import { DefaultEncryptionService, LdapEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { FileSystemAdapter } from '@infra/file-system'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { StorageProviderEntity, SystemEntity } from '@shared/domain/entity'; +import { StorageProviderEntity } from '@shared/domain/entity'; import { setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { BsonConverter } from '../converter/bson.converter'; 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 8ec0dfcb436..e9422066b67 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.ts @@ -2,13 +2,14 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { DatabaseManagementService } from '@infra/database'; import { DefaultEncryptionService, EncryptionService, LdapEncryptionService } from '@infra/encryption'; import { FileSystemAdapter } from '@infra/file-system'; +import { UmzugMigration } from '@mikro-orm/migrations-mongodb'; import { EntityManager } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { StorageProviderEntity, SystemEntity } from '@shared/domain/entity'; +import { StorageProviderEntity } from '@shared/domain/entity'; import { LegacyLogger } from '@src/core/logger'; import { orderBy } from 'lodash'; -import { UmzugMigration } from '@mikro-orm/migrations-mongodb'; import { BsonConverter } from '../converter/bson.converter'; import { generateSeedData } from '../seed-data/generateSeedData'; 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 3ea58e99d72..cd6d68b77f0 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts @@ -8,6 +8,9 @@ import { HydraOauthUc } from '../uc'; import { AuthorizationParams } from './dto'; import { StatelessAuthorizationParams } from './dto/stateless-authorization.params'; +/** + * @deprecated To be removed in N21-2071 + */ @ApiTags('SSO') @Controller('sso') export class OauthSSOController { 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 9874bbb22da..450529bd1ae 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.spec.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.spec.ts @@ -5,11 +5,12 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { HydraSsoService } from '@modules/oauth/service/hydra.service'; +import { OauthConfigEntity } from '@modules/system/entity'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { LtiPrivacyPermission, LtiRoleType, OauthConfigEntity } from '@shared/domain/entity'; +import { LtiPrivacyPermission, LtiRoleType } from '@shared/domain/entity'; 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 ca439858e9f..04e93b1a2d7 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.ts @@ -3,18 +3,21 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { AuthorizationParams } from '@modules/oauth/controller/dto/authorization.params'; import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; +import { OauthConfigEntity } from '@modules/system/entity'; import { HttpService } from '@nestjs/axios'; import { Inject, InternalServerErrorException } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { OauthConfigEntity } from '@shared/domain/entity'; import { LtiToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { nanoid } from 'nanoid'; import QueryString from 'qs'; -import { Observable, firstValueFrom } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; +/** + * @deprecated To be removed in N21-2071 + */ @Injectable() export class HydraSsoService { constructor( 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 de9246db5e7..b31f8c38c99 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -4,19 +4,18 @@ import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionServi import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; import { ProvisioningService } from '@modules/provisioning'; -import { OauthConfigDto } from '@modules/system/service'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { OauthConfigEntity } from '@modules/system/entity'; +import { SystemService } from '@modules/system/service'; import { UserService } from '@modules/user'; import { MigrationCheckService } from '@modules/user-login-migration'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; -import { OauthConfigEntity, SystemEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { SchoolFeature } from '@shared/domain/types'; -import { legacySchoolDoFactory, setupEntities, systemEntityFactory, userDoFactory } from '@shared/testing'; +import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { OauthDataDto } from '@src/modules/provisioning/dto'; -import { LegacySystemService } from '@src/modules/system'; +import { System } from '@src/modules/system'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { OAuthTokenDto } from '../interface'; import { @@ -49,12 +48,12 @@ describe('OAuthService', () => { let oAuthEncryptionService: DeepMocked; let provisioningService: DeepMocked; let userService: DeepMocked; - let systemService: DeepMocked; + let systemService: DeepMocked; let oauthAdapterService: DeepMocked; let migrationCheckService: DeepMocked; let schoolService: DeepMocked; - let testSystem: SystemEntity; + let testSystem: System; let testOauthConfig: OauthConfigEntity; const hostUri = 'https://mock.de'; @@ -86,8 +85,8 @@ describe('OAuthService', () => { useValue: createMock(), }, { - provide: LegacySystemService, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: OauthAdapterService, @@ -104,7 +103,7 @@ describe('OAuthService', () => { oAuthEncryptionService = module.get(DefaultEncryptionService); provisioningService = module.get(ProvisioningService); userService = module.get(UserService); - systemService = module.get(LegacySystemService); + systemService = module.get(SystemService); oauthAdapterService = module.get(OauthAdapterService); migrationCheckService = module.get(MigrationCheckService); schoolService = module.get(LegacySchoolService); @@ -129,7 +128,7 @@ describe('OAuthService', () => { } }); - testSystem = systemEntityFactory.withOauthConfig().buildWithId(); + testSystem = systemFactory.withOauthConfig().build(); testOauthConfig = testSystem.oauthConfig as OauthConfigEntity; }); @@ -200,24 +199,12 @@ describe('OAuthService', () => { const setup = () => { const authCode = '43534543jnj543342jn2'; - const oauthConfig: OauthConfigDto = new OauthConfigDto({ - clientId: '12345', - clientSecret: 'mocksecret', - tokenEndpoint: 'http://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: System = systemFactory.withOauthConfig().build({ + displayName: 'External System', }); - const system: SystemDto = new SystemDto({ - id: 'systemId', - type: 'oauth', - oauthConfig, + + const ldapSystem: System = systemFactory.withLdapConfig().build({ + displayName: 'External System', }); const oauthToken: OAuthTokenDto = { @@ -230,7 +217,7 @@ describe('OAuthService', () => { authCode, system, oauthToken, - oauthConfig, + ldapSystem, }; }; @@ -242,7 +229,7 @@ describe('OAuthService', () => { oauthAdapterService.getPublicKey.mockResolvedValue('publicKey'); oauthAdapterService.sendTokenRequest.mockResolvedValue(oauthToken); - const result: OAuthTokenDto = await service.authenticateUser(system.id!, 'redirectUri', authCode); + const result: OAuthTokenDto = await service.authenticateUser(system.id, 'redirectUri', authCode); expect(result).toEqual({ accessToken: oauthToken.accessToken, @@ -254,10 +241,9 @@ describe('OAuthService', () => { describe('when system does not have oauth config', () => { it('the authentication should fail', async () => { - const { authCode, system } = setup(); - system.oauthConfig = undefined; + const { authCode, ldapSystem } = setup(); - systemService.findById.mockResolvedValueOnce(system); + systemService.findById.mockResolvedValueOnce(ldapSystem); const func = () => service.authenticateUser(testSystem.id, 'redirectUri', authCode); diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index ebba4c64e66..4b8eb53afb8 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -2,14 +2,13 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { LegacySchoolService } from '@modules/legacy-school'; import { OauthDataDto } from '@modules/provisioning/dto/oauth-data.dto'; import { ProvisioningService } from '@modules/provisioning/service/provisioning.service'; -import { LegacySystemService } from '@modules/system'; -import { SystemDto } from '@modules/system/service'; +import { System, SystemService } from '@modules/system'; +import { OauthConfigEntity } from '@modules/system/entity'; import { UserService } from '@modules/user'; import { MigrationCheckService } from '@modules/user-login-migration'; import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; -import { OauthConfigEntity } from '@shared/domain/entity'; import { EntityId, SchoolFeature } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; import jwt, { JwtPayload } from 'jsonwebtoken'; @@ -31,7 +30,7 @@ export class OAuthService { @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService, private readonly logger: LegacyLogger, private readonly provisioningService: ProvisioningService, - private readonly systemService: LegacySystemService, + private readonly systemService: SystemService, private readonly migrationCheckService: MigrationCheckService, private readonly schoolService: LegacySchoolService ) { @@ -39,9 +38,9 @@ export class OAuthService { } async authenticateUser(systemId: string, redirectUri: string, code: string): Promise { - const system: SystemDto = await this.systemService.findById(systemId); + const system: System | null = await this.systemService.findById(systemId); - if (!system.oauthConfig) { + if (!system || !system.oauthConfig) { throw new OauthConfigMissingLoggableException(systemId); } const { oauthConfig } = system; 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 efaf7190329..269c69753e9 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 @@ -2,10 +2,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HydraSsoService, OAuthService } from '@modules/oauth'; import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; +import { OauthConfigEntity } from '@modules/system/entity'; import { HttpModule } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { OauthConfigEntity } from '@shared/domain/entity'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; 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 7b92362cd57..596aaf2e475 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts @@ -1,13 +1,16 @@ -import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; +import { OauthConfigEntity } from '@modules/system/entity'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { OauthConfigEntity } from '@shared/domain/entity'; import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { AuthorizationParams } from '../controller/dto'; import { OAuthTokenDto } from '../interface'; import { AuthCodeFailureLoggableException } from '../loggable'; import { HydraSsoService, OAuthService } from '../service'; +import { HydraRedirectDto } from '../service/dto'; +/** + * @deprecated To be removed in N21-2071 + */ @Injectable() export class HydraOauthUc { constructor( diff --git a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts index f25054ce1ac..7de363b3ef1 100644 --- a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts @@ -1,34 +1,36 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { systemFactory } from '@shared/testing'; import { ProvisioningSystemDto } from '../dto'; import { ProvisioningSystemInputMapper } from './provisioning-system-input.mapper'; describe('SchoolUcMapper', () => { describe('mapToInternal', () => { it('should map provisioningStrategy', () => { - const dto: SystemDto = new SystemDto({ - id: 'systemId', - type: '', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - provisioningUrl: 'provisioningUrl', + const provisioningStrategy = SystemProvisioningStrategy.SANIS; + const system = systemFactory.build({ + provisioningStrategy, + provisioningUrl: 'https://prov.url', }); - const result: ProvisioningSystemDto = ProvisioningSystemInputMapper.mapToInternal(dto); + const result: ProvisioningSystemDto = ProvisioningSystemInputMapper.mapToInternal(system); expect(result).toEqual({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - provisioningUrl: 'provisioningUrl', + systemId: system.id, + provisioningStrategy, + provisioningUrl: system.provisioningUrl, }); }); it('should map provisioningStrategy, when input undefined', () => { - const dto: SystemDto = new SystemDto({ type: '' }); + const system = systemFactory.build({ + provisioningStrategy: undefined, + provisioningUrl: undefined, + }); - const result: ProvisioningSystemDto = ProvisioningSystemInputMapper.mapToInternal(dto); + const result: ProvisioningSystemDto = ProvisioningSystemInputMapper.mapToInternal(system); expect(result).toEqual({ - systemId: '', + systemId: system.id, provisioningStrategy: SystemProvisioningStrategy.UNDEFINED, provisioningUrl: undefined, }); diff --git a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts index 9668bbfffea..ca693762dca 100644 --- a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts +++ b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts @@ -1,13 +1,13 @@ +import { System } from '@modules/system'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; import { ProvisioningSystemDto } from '../dto'; export class ProvisioningSystemInputMapper { - static mapToInternal(dto: SystemDto) { + static mapToInternal(dto: System): ProvisioningSystemDto { return new ProvisioningSystemDto({ - systemId: dto.id || '', + systemId: dto.id, provisioningStrategy: dto.provisioningStrategy || SystemProvisioningStrategy.UNDEFINED, - provisioningUrl: dto.provisioningUrl || undefined, + provisioningUrl: dto.provisioningUrl, }); } } diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 1664c649b10..170ce718ef5 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { System, SystemService } from '@modules/system'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { LegacySystemService } from '../../system/service/legacy-system.service'; +import { systemFactory } from '@shared/testing'; import { ExternalUserDto, OauthDataDto, @@ -18,7 +18,7 @@ describe('ProvisioningService', () => { let module: TestingModule; let service: ProvisioningService; - let systemService: DeepMocked; + let systemService: DeepMocked; let provisioningStrategy: DeepMocked; beforeAll(async () => { @@ -26,8 +26,8 @@ describe('ProvisioningService', () => { providers: [ ProvisioningService, { - provide: LegacySystemService, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: SanisProvisioningStrategy, @@ -57,7 +57,7 @@ describe('ProvisioningService', () => { }).compile(); service = module.get(ProvisioningService); - systemService = module.get(LegacySystemService); + systemService = module.get(SystemService); provisioningStrategy = module.get(SanisProvisioningStrategy); }); @@ -70,16 +70,13 @@ describe('ProvisioningService', () => { }); const setupSystemData = () => { - const systemId = 'sanisSystemId'; - const system: SystemDto = new SystemDto({ - id: systemId, - type: 'sanis', - provisioningUrl: 'sanisUrl', + const system: System = systemFactory.withOauthConfig().build({ + provisioningUrl: 'https://api.moin.schule/', provisioningStrategy: SystemProvisioningStrategy.SANIS, }); const provisioningSystemDto: ProvisioningSystemDto = new ProvisioningSystemDto({ - systemId, - provisioningUrl: 'sanisUrl', + systemId: system.id, + provisioningUrl: 'https://api.moin.schule/', provisioningStrategy: SystemProvisioningStrategy.SANIS, }); const oauthDataDto: OauthDataDto = new OauthDataDto({ @@ -93,7 +90,6 @@ describe('ProvisioningService', () => { }); return { - systemId, system, provisioningSystemDto, oauthDataDto, @@ -103,17 +99,16 @@ describe('ProvisioningService', () => { describe('getData is called', () => { const setup = () => { - const { systemId, system, provisioningSystemDto, oauthDataDto } = setupSystemData(); + const { system, provisioningSystemDto, oauthDataDto } = setupSystemData(); const accessToken = 'accessToken'; const idToken = 'idToken'; - systemService.findById.mockResolvedValue(system); + systemService.findByIdOrFail.mockResolvedValue(system); provisioningStrategy.getData.mockResolvedValue(oauthDataDto); return { accessToken, idToken, - systemId, system, provisioningSystemDto, oauthDataDto, @@ -122,9 +117,9 @@ describe('ProvisioningService', () => { describe('when the provisioning strategy is found', () => { it('should call strategy.getData', async () => { - const { accessToken, idToken, systemId, provisioningSystemDto } = setup(); + const { accessToken, idToken, system, provisioningSystemDto } = setup(); - await service.getData(systemId, idToken, accessToken); + await service.getData(system.id, idToken, accessToken); expect(provisioningStrategy.getData).toHaveBeenCalledWith( new OauthDataStrategyInputDto({ @@ -136,9 +131,9 @@ describe('ProvisioningService', () => { }); it('should return the oauth data', async () => { - const { accessToken, idToken, systemId, oauthDataDto } = setup(); + const { accessToken, idToken, system, oauthDataDto } = setup(); - const result: OauthDataDto = await service.getData(systemId, idToken, accessToken); + const result: OauthDataDto = await service.getData(system.id, idToken, accessToken); expect(result).toEqual(oauthDataDto); }); @@ -147,12 +142,11 @@ describe('ProvisioningService', () => { describe('when no provisioning strategy is found', () => { it('should throw an InternalServerErrorException', async () => { const { accessToken, idToken } = setup(); - const systemWithoutStrategy: SystemDto = new SystemDto({ - type: '', + const systemWithoutStrategy: System = systemFactory.withOauthConfig().build({ provisioningStrategy: SystemProvisioningStrategy.UNDEFINED, }); - systemService.findById.mockResolvedValue(systemWithoutStrategy); + systemService.findByIdOrFail.mockResolvedValue(systemWithoutStrategy); const promise: Promise = service.getData('systemId', idToken, accessToken); diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.ts b/apps/server/src/modules/provisioning/service/provisioning.service.ts index 8f7330645b5..3aefef535a3 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.ts @@ -1,5 +1,4 @@ -import { LegacySystemService } from '@modules/system'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { System, SystemService } from '@modules/system'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; @@ -19,7 +18,7 @@ export class ProvisioningService { >(); constructor( - private readonly systemService: LegacySystemService, + private readonly systemService: SystemService, private readonly sanisStrategy: SanisProvisioningStrategy, private readonly iservStrategy: IservProvisioningStrategy, private readonly oidcMockStrategy: OidcMockProvisioningStrategy @@ -48,8 +47,10 @@ export class ProvisioningService { } private async determineInput(systemId: string): Promise { - const systemDto: SystemDto = await this.systemService.findById(systemId); + const systemDto: System = await this.systemService.findByIdOrFail(systemId); + const inputDto: ProvisioningSystemDto = ProvisioningSystemInputMapper.mapToInternal(systemDto); + return inputDto; } diff --git a/apps/server/src/modules/school/api/test/school-patch.api.spec.ts b/apps/server/src/modules/school/api/test/school-patch.api.spec.ts index 336aae739e8..eb255861f24 100644 --- a/apps/server/src/modules/school/api/test/school-patch.api.spec.ts +++ b/apps/server/src/modules/school/api/test/school-patch.api.spec.ts @@ -1,16 +1,17 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { - TestApiClient, - UserAndAccountTestFactory, cleanupCollections, countyEmbeddableFactory, federalStateFactory, schoolEntityFactory, schoolYearFactory, systemEntityFactory, + TestApiClient, + UserAndAccountTestFactory, } from '@shared/testing'; import { ServerTestModule } from '@src/modules/server'; import { SchoolErrorEnum } from '../../domain/error'; diff --git a/apps/server/src/modules/school/domain/do/school.ts b/apps/server/src/modules/school/domain/do/school.ts index 0ae47a767e6..45ea43d255d 100644 --- a/apps/server/src/modules/school/domain/do/school.ts +++ b/apps/server/src/modules/school/domain/do/school.ts @@ -20,6 +20,18 @@ interface SchoolInfo { } export class School extends DomainObject { + get systems(): EntityId[] { + return this.props.systemIds; + } + + set externalId(externalId: string | undefined) { + this.props.externalId = externalId; + } + + set ldapLastSync(ldapLastSync: string | undefined) { + this.props.ldapLastSync = ldapLastSync; + } + public getInfo(): SchoolInfo { const info = { id: this.props.id, @@ -100,15 +112,13 @@ export class School extends DomainObject { public hasSystem(systemId: EntityId): boolean { const { systemIds } = this.props; - const result = systemIds?.includes(systemId) ?? false; + const result = systemIds.includes(systemId); return result; } public removeSystem(systemId: EntityId) { - if (this.props.systemIds) { - this.props.systemIds = this.props.systemIds.filter((id) => id !== systemId); - } + this.props.systemIds = this.props.systemIds.filter((id) => id !== systemId); } } @@ -127,7 +137,7 @@ export interface SchoolProps extends AuthorizableObject { purpose?: SchoolPurpose; features: Set; instanceFeatures?: Set; - systemIds?: EntityId[]; + systemIds: EntityId[]; logo?: SchoolLogo; fileStorageType?: FileStorageType; language?: LanguageType; @@ -137,4 +147,5 @@ export interface SchoolProps extends AuthorizableObject { // It can't be mapped to a feature straight-forwardly, // because the config value STUDENT_TEAM_CREATION has to be taken into account. enableStudentTeamCreation?: boolean; + ldapLastSync?: string; } diff --git a/apps/server/src/modules/school/domain/event-handler/index.ts b/apps/server/src/modules/school/domain/event-handler/index.ts new file mode 100644 index 00000000000..6b3cdd17830 --- /dev/null +++ b/apps/server/src/modules/school/domain/event-handler/index.ts @@ -0,0 +1 @@ +export { SystemDeletedHandler } from './system-deleted.handler'; diff --git a/apps/server/src/modules/school/domain/event-handler/system-deleted.handler.spec.ts b/apps/server/src/modules/school/domain/event-handler/system-deleted.handler.spec.ts new file mode 100644 index 00000000000..e4372297239 --- /dev/null +++ b/apps/server/src/modules/school/domain/event-handler/system-deleted.handler.spec.ts @@ -0,0 +1,107 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { SystemDeletedEvent } from '@modules/system'; +import { Test, TestingModule } from '@nestjs/testing'; +import { systemFactory } from '@shared/testing'; +import { schoolFactory } from '../../testing'; +import { School } from '../do'; +import { SchoolService } from '../service'; +import { SystemDeletedHandler } from './system-deleted.handler'; + +describe(SystemDeletedHandler.name, () => { + let module: TestingModule; + let handler: SystemDeletedHandler; + + let schoolService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + SystemDeletedHandler, + { + provide: SchoolService, + useValue: createMock(), + }, + ], + }).compile(); + + handler = module.get(SystemDeletedHandler); + schoolService = module.get(SchoolService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('handle', () => { + describe('when a non-ldap system is removed', () => { + const setup = () => { + const system = systemFactory.withOauthConfig().build(); + const school = schoolFactory.build({ + systemIds: [system.id], + ldapLastSync: new Date().toISOString(), + externalId: 'schoolExternalId', + }); + const event = new SystemDeletedEvent({ schoolId: school.id, system }); + + schoolService.getSchoolById.mockResolvedValueOnce(new School({ ...school.getProps() })); + + return { + school, + event, + }; + }; + + it('should should remove the system and save the school', async () => { + const { school, event } = setup(); + + await handler.handle(event); + + expect(schoolService.save).toHaveBeenCalledWith( + new School({ + ...school.getProps(), + systemIds: [], + ldapLastSync: undefined, + }) + ); + }); + }); + + describe('when the last ldap system is removed', () => { + const setup = () => { + const system = systemFactory.withLdapConfig().build(); + const school = schoolFactory.build({ + systemIds: [system.id], + ldapLastSync: new Date().toISOString(), + externalId: 'schoolExternalId', + }); + const event = new SystemDeletedEvent({ schoolId: school.id, system }); + + schoolService.getSchoolById.mockResolvedValueOnce(new School({ ...school.getProps() })); + + return { + school, + event, + }; + }; + + it('should should remove the system and save the school', async () => { + const { school, event } = setup(); + + await handler.handle(event); + + expect(schoolService.save).toHaveBeenCalledWith( + new School({ + ...school.getProps(), + systemIds: [], + ldapLastSync: undefined, + externalId: undefined, + }) + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/school/domain/event-handler/system-deleted.handler.ts b/apps/server/src/modules/school/domain/event-handler/system-deleted.handler.ts new file mode 100644 index 00000000000..6868ce0cf89 --- /dev/null +++ b/apps/server/src/modules/school/domain/event-handler/system-deleted.handler.ts @@ -0,0 +1,24 @@ +import { SystemDeletedEvent, SystemType } from '@modules/system/domain'; +import { Injectable } from '@nestjs/common'; +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { School } from '../do'; +import { SchoolService } from '../service'; + +@Injectable() +@EventsHandler(SystemDeletedEvent) +export class SystemDeletedHandler implements IEventHandler { + constructor(private readonly schoolService: SchoolService) {} + + public async handle(event: SystemDeletedEvent): Promise { + const school: School = await this.schoolService.getSchoolById(event.schoolId); + + school.removeSystem(event.system.id); + school.ldapLastSync = undefined; + + if (event.system.type === SystemType.LDAP && school.systems.length === 0) { + school.externalId = undefined; + } + + await this.schoolService.save(school); + } +} diff --git a/apps/server/src/modules/school/domain/factory/school.factory.spec.ts b/apps/server/src/modules/school/domain/factory/school.factory.spec.ts index f01c10e257f..eba0e00d007 100644 --- a/apps/server/src/modules/school/domain/factory/school.factory.spec.ts +++ b/apps/server/src/modules/school/domain/factory/school.factory.spec.ts @@ -1,14 +1,13 @@ import { LanguageType } from '@shared/domain/interface'; import { SchoolFeature } from '@shared/domain/types'; -import { federalStateFactory } from '../../testing'; -import { School } from '../do'; +import { federalStateFactory, schoolFactory } from '../../testing'; import { FileStorageType } from '../type'; import { SchoolFactory } from './school.factory'; describe('SchoolFactory', () => { describe('buildFromPartialBody', () => { const buildSchool = () => { - const school = new School({ + const school = schoolFactory.build({ id: 'school-id', name: 'school-name', officialSchoolNumber: 'school-number', diff --git a/apps/server/src/modules/school/domain/service/school.service.spec.ts b/apps/server/src/modules/school/domain/service/school.service.spec.ts index 8c2ecbd0e6d..556b4ee89be 100644 --- a/apps/server/src/modules/school/domain/service/school.service.spec.ts +++ b/apps/server/src/modules/school/domain/service/school.service.spec.ts @@ -510,6 +510,36 @@ describe('SchoolService', () => { }); }); + describe('save', () => { + describe('when saving a school', () => { + const setup = () => { + const school = schoolFactory.build({ name: 'old name' }); + + schoolRepo.save.mockResolvedValueOnce(school); + + return { + school, + }; + }; + + it('should save the school', async () => { + const { school } = setup(); + + await service.save(school); + + expect(schoolRepo.save).toHaveBeenCalledWith(school); + }); + + it('should return the updated school', async () => { + const { school } = setup(); + + const result = await service.save(school); + + expect(result).toEqual(school); + }); + }); + }); + describe('getSchoolSystems', () => { describe('when school has systems', () => { const setup = () => { diff --git a/apps/server/src/modules/school/domain/service/school.service.ts b/apps/server/src/modules/school/domain/service/school.service.ts index cf22c9a7dd0..b590a6250f5 100644 --- a/apps/server/src/modules/school/domain/service/school.service.ts +++ b/apps/server/src/modules/school/domain/service/school.service.ts @@ -1,14 +1,17 @@ +import { System, SystemService } from '@modules/system'; import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeGuard } from '@shared/common'; import { IFindOptions } from '@shared/domain/interface/find-options'; import { EntityId } from '@shared/domain/types/entity-id'; -import { System, SystemService } from '@src/modules/system'; import { SchoolConfig } from '../../school.config'; import { School, SchoolProps, SystemForLdapLogin } from '../do'; import { SchoolForLdapLogin, SchoolForLdapLoginProps } from '../do/school-for-ldap-login'; -import { SchoolHasNoSystemLoggableException, SystemCanNotBeDeletedLoggableException } from '../error'; -import { SystemNotFoundLoggableException } from '../error/system-not-found.loggable-exception'; +import { + SchoolHasNoSystemLoggableException, + SystemCanNotBeDeletedLoggableException, + SystemNotFoundLoggableException, +} from '../error'; import { SchoolFactory } from '../factory'; import { SCHOOL_REPO, SchoolRepo, SchoolUpdateBody } from '../interface'; import { SchoolQuery } from '../query'; @@ -89,7 +92,7 @@ export class SchoolService { return schoolsForLdapLogin; } - public async updateSchool(school: School, body: SchoolUpdateBody) { + public async updateSchool(school: School, body: SchoolUpdateBody): Promise { const fullSchoolObject = SchoolFactory.buildFromPartialBody(school, body); let updatedSchool = await this.schoolRepo.save(fullSchoolObject); @@ -98,6 +101,12 @@ export class SchoolService { return updatedSchool; } + public async save(school: School): Promise { + const updatedSchool: School = await this.schoolRepo.save(school); + + return updatedSchool; + } + public async removeSystemFromSchool(school: School, systemId: EntityId): Promise { if (!school.hasSystem(systemId)) { throw new SchoolHasNoSystemLoggableException(school.id, systemId); @@ -110,7 +119,7 @@ export class SchoolService { await this.schoolRepo.save(school); } - private async tryFindAndRemoveSystem(systemId: string) { + private async tryFindAndRemoveSystem(systemId: string): Promise { const system = await this.systemService.findById(systemId); if (!system) { throw new SystemNotFoundLoggableException(systemId); diff --git a/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts b/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts index d045785e78e..b1838ae2185 100644 --- a/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts +++ b/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts @@ -1,4 +1,4 @@ -import { SystemEntity } from '@shared/domain/entity'; +import { SystemEntity } from '@modules/system/entity'; import { schoolEntityFactory, setupEntities } from '@shared/testing'; import { School } from '../../../domain'; import { CountyEmbeddableMapper } from './county.embeddable.mapper'; diff --git a/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.ts b/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.ts index 993d3897216..7d5bc0f8396 100644 --- a/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.ts +++ b/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.ts @@ -1,6 +1,7 @@ import { EntityData } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; -import { FederalStateEntity, SchoolYearEntity, SystemEntity } from '@shared/domain/entity'; +import { SystemEntity } from '@modules/system/entity'; +import { FederalStateEntity, SchoolYearEntity } from '@shared/domain/entity'; import { SchoolEntity } from '@shared/domain/entity/school.entity'; import { SchoolFactory } from '@src/modules/school/domain/factory'; import { School } from '../../../domain'; @@ -39,6 +40,7 @@ export class SchoolEntityMapper { federalState, features, county, + ldapLastSync: entity.ldapLastSync, }); return school; diff --git a/apps/server/src/modules/school/school.module.ts b/apps/server/src/modules/school/school.module.ts index d9ebac6e83e..dbb158d633e 100644 --- a/apps/server/src/modules/school/school.module.ts +++ b/apps/server/src/modules/school/school.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { SystemModule } from '../system'; import { SCHOOL_REPO, SCHOOL_YEAR_REPO, SchoolService, SchoolYearService } from './domain'; +import { SystemDeletedHandler } from './domain/event-handler'; import { SchoolYearMikroOrmRepo } from './repo/mikro-orm/school-year.repo'; import { SchoolMikroOrmRepo } from './repo/mikro-orm/school.repo'; @@ -11,6 +12,7 @@ import { SchoolMikroOrmRepo } from './repo/mikro-orm/school.repo'; SchoolYearService, { provide: SCHOOL_REPO, useClass: SchoolMikroOrmRepo }, { provide: SCHOOL_YEAR_REPO, useClass: SchoolYearMikroOrmRepo }, + SystemDeletedHandler, ], exports: [SchoolService, SchoolYearService], }) diff --git a/apps/server/src/modules/school/testing/school.factory.ts b/apps/server/src/modules/school/testing/school.factory.ts index ccb05fde5ee..e44195c9cbb 100644 --- a/apps/server/src/modules/school/testing/school.factory.ts +++ b/apps/server/src/modules/school/testing/school.factory.ts @@ -1,6 +1,6 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { SchoolFeature } from '@shared/domain/types'; import { BaseFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; import { School, SchoolProps } from '../domain'; import { federalStateFactory } from './federal-state.factory'; @@ -12,5 +12,6 @@ export const schoolFactory = BaseFactory.define(School, ({ name: `school #${sequence}`, federalState: federalStateFactory.build(), features: new Set(), + systemIds: [], }; }); diff --git a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts index 4aaf7fff046..6631d18c2c9 100644 --- a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts +++ b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts @@ -2,9 +2,10 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { OauthConfigEntity, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { schoolEntityFactory, systemEntityFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { Response } from 'supertest'; +import { OauthConfigEntity, SystemEntity } from '../../entity'; import { PublicSystemListResponse, PublicSystemResponse } from '../dto'; const baseRouteName = '/systems'; diff --git a/apps/server/src/modules/system/controller/dto/oauth-config.response.ts b/apps/server/src/modules/system/controller/dto/oauth-config.response.ts index f2649b75f9a..fabc3b95f21 100644 --- a/apps/server/src/modules/system/controller/dto/oauth-config.response.ts +++ b/apps/server/src/modules/system/controller/dto/oauth-config.response.ts @@ -85,20 +85,7 @@ export class OauthConfigResponse { }) jwksEndpoint: string; - constructor(oauthConfigResponse: { - redirectUri: string; - idpHint?: string; - tokenEndpoint: string; - responseType: string; - clientId: string; - provider: string; - jwksEndpoint: string; - authEndpoint: string; - scope: string; - logoutEndpoint?: string; - grantType: string; - issuer: string; - }) { + constructor(oauthConfigResponse: OauthConfigResponse) { this.clientId = oauthConfigResponse.clientId; this.idpHint = oauthConfigResponse.idpHint; this.redirectUri = oauthConfigResponse.redirectUri; diff --git a/apps/server/src/modules/system/controller/dto/public-system-response.ts b/apps/server/src/modules/system/controller/dto/public-system-response.ts index b00055a1849..c562b1a8729 100644 --- a/apps/server/src/modules/system/controller/dto/public-system-response.ts +++ b/apps/server/src/modules/system/controller/dto/public-system-response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { OauthConfigResponse } from '@modules/system/controller/dto/oauth-config.response'; +import { OauthConfigResponse } from './oauth-config.response'; export class PublicSystemResponse { @ApiProperty({ diff --git a/apps/server/src/modules/system/controller/dto/system.filter.params.ts b/apps/server/src/modules/system/controller/dto/system.filter.params.ts index c2f49f55231..6cd966eb5bb 100644 --- a/apps/server/src/modules/system/controller/dto/system.filter.params.ts +++ b/apps/server/src/modules/system/controller/dto/system.filter.params.ts @@ -1,17 +1,13 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { StringToBoolean } from '@shared/controller'; -import { SystemTypeEnum } from '@shared/domain/types'; -import { IsBoolean, IsEnum, IsOptional } from 'class-validator'; +import { SingleValueToArrayTransformer } from '@shared/controller'; +import { IsArray, IsEnum, IsOptional } from 'class-validator'; +import { SystemType } from '../../domain'; export class SystemFilterParams { - @ApiPropertyOptional({ description: 'The type of the system.' }) + @ApiPropertyOptional({ description: 'The type of the system.', enum: SystemType, enumName: 'SystemType' }) + @SingleValueToArrayTransformer() @IsOptional() - @IsEnum(SystemTypeEnum) - type?: SystemTypeEnum; - - @ApiPropertyOptional({ description: 'Flag to request only systems with oauth-config.' }) - @IsOptional() - @IsBoolean() - @StringToBoolean() - onlyOauth?: boolean; + @IsArray() + @IsEnum(SystemType, { each: true }) + types?: SystemType[]; } diff --git a/apps/server/src/modules/system/controller/index.ts b/apps/server/src/modules/system/controller/index.ts new file mode 100644 index 00000000000..59c1fa7c892 --- /dev/null +++ b/apps/server/src/modules/system/controller/index.ts @@ -0,0 +1 @@ +export { SystemController } from './system.controller'; diff --git a/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts b/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts index 20a215a3dbe..0e4c4da8438 100644 --- a/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts +++ b/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts @@ -1,13 +1,10 @@ -import { OauthConfigResponse } from '@modules/system/controller/dto/oauth-config.response'; -import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { PublicSystemListResponse } from '../dto/public-system-list.response'; -import { PublicSystemResponse } from '../dto/public-system-response'; +import { OauthConfig, System } from '../../domain'; +import { OauthConfigResponse, PublicSystemListResponse, PublicSystemResponse } from '../dto'; export class SystemResponseMapper { - static mapFromDtoToListResponse(systems: SystemDto[]): PublicSystemListResponse { + static mapFromDtoToListResponse(systems: System[]): PublicSystemListResponse { const systemResponses: PublicSystemResponse[] = systems.map( - (system: SystemDto): PublicSystemResponse => this.mapFromDtoToResponse(system) + (system: System): PublicSystemResponse => this.mapFromDtoToResponse(system) ); const systemListResponse: PublicSystemListResponse = new PublicSystemListResponse(systemResponses); @@ -15,9 +12,9 @@ export class SystemResponseMapper { return systemListResponse; } - static mapFromDtoToResponse(system: SystemDto): PublicSystemResponse { + static mapFromDtoToResponse(system: System): PublicSystemResponse { const systemResponse: PublicSystemResponse = new PublicSystemResponse({ - id: system.id || '', + id: system.id, type: system.type, alias: system.alias, displayName: system.displayName, @@ -29,21 +26,21 @@ export class SystemResponseMapper { return systemResponse; } - static mapFromOauthConfigDtoToResponse(oauthConfigDto: OauthConfigDto): OauthConfigResponse { + static mapFromOauthConfigDtoToResponse(oauthConfig: OauthConfig): OauthConfigResponse { const oauthConfigResponse: OauthConfigResponse = new OauthConfigResponse({ - clientId: oauthConfigDto.clientId, + clientId: oauthConfig.clientId, // clientSecret will not be mapped for security reasons, - idpHint: oauthConfigDto.idpHint, - redirectUri: oauthConfigDto.redirectUri, - grantType: oauthConfigDto.grantType, - tokenEndpoint: oauthConfigDto.tokenEndpoint, - authEndpoint: oauthConfigDto.authEndpoint, - responseType: oauthConfigDto.responseType, - scope: oauthConfigDto.scope, - provider: oauthConfigDto.provider, - logoutEndpoint: oauthConfigDto.logoutEndpoint, - issuer: oauthConfigDto.issuer, - jwksEndpoint: oauthConfigDto.jwksEndpoint, + idpHint: oauthConfig.idpHint, + redirectUri: oauthConfig.redirectUri, + grantType: oauthConfig.grantType, + tokenEndpoint: oauthConfig.tokenEndpoint, + authEndpoint: oauthConfig.authEndpoint, + responseType: oauthConfig.responseType, + scope: oauthConfig.scope, + provider: oauthConfig.provider, + logoutEndpoint: oauthConfig.logoutEndpoint, + issuer: oauthConfig.issuer, + jwksEndpoint: oauthConfig.jwksEndpoint, }); return oauthConfigResponse; diff --git a/apps/server/src/modules/system/controller/system.controller.ts b/apps/server/src/modules/system/controller/system.controller.ts index a0630a8119a..ab115a0efdd 100644 --- a/apps/server/src/modules/system/controller/system.controller.ts +++ b/apps/server/src/modules/system/controller/system.controller.ts @@ -1,7 +1,7 @@ import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Delete, Get, HttpCode, HttpStatus, Param, Query } from '@nestjs/common'; import { ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; -import { SystemDto } from '../service'; +import { System } from '../domain'; import { SystemUc } from '../uc/system.uc'; import { PublicSystemListResponse, PublicSystemResponse, SystemFilterParams, SystemIdParams } from './dto'; import { SystemResponseMapper } from './mapper/system-response.mapper'; @@ -19,9 +19,9 @@ export class SystemController { @ApiOperation({ summary: 'Finds all publicly available systems.' }) @ApiResponse({ status: 200, type: PublicSystemListResponse, description: 'Returns a list of systems.' }) async find(@Query() filterParams: SystemFilterParams): Promise { - const systemDtos: SystemDto[] = await this.systemUc.findByFilter(filterParams.type, filterParams.onlyOauth); + const systems: System[] = await this.systemUc.find(filterParams.types); - const mapped: PublicSystemListResponse = SystemResponseMapper.mapFromDtoToListResponse(systemDtos); + const mapped: PublicSystemListResponse = SystemResponseMapper.mapFromDtoToListResponse(systems); return mapped; } @@ -34,9 +34,9 @@ export class SystemController { @ApiOperation({ summary: 'Finds a publicly available system.' }) @ApiResponse({ status: 200, type: PublicSystemResponse, description: 'Returns a system.' }) async getSystem(@Param() params: SystemIdParams): Promise { - const systemDto: SystemDto = await this.systemUc.findById(params.systemId); + const system: System = await this.systemUc.findById(params.systemId); - const mapped: PublicSystemResponse = SystemResponseMapper.mapFromDtoToResponse(systemDto); + const mapped: PublicSystemResponse = SystemResponseMapper.mapFromDtoToResponse(system); return mapped; } diff --git a/apps/server/src/modules/system/domain/event/index.ts b/apps/server/src/modules/system/domain/event/index.ts new file mode 100644 index 00000000000..8e3763014d0 --- /dev/null +++ b/apps/server/src/modules/system/domain/event/index.ts @@ -0,0 +1 @@ +export { SystemDeletedEvent } from './system-deleted.event'; diff --git a/apps/server/src/modules/system/domain/event/system-deleted.event.ts b/apps/server/src/modules/system/domain/event/system-deleted.event.ts new file mode 100644 index 00000000000..77324739bcf --- /dev/null +++ b/apps/server/src/modules/system/domain/event/system-deleted.event.ts @@ -0,0 +1,13 @@ +import { EntityId } from '@shared/domain/types'; +import { System } from '../system.do'; + +export class SystemDeletedEvent { + schoolId: EntityId; + + system: System; + + constructor(props: SystemDeletedEvent) { + this.schoolId = props.schoolId; + this.system = props.system; + } +} diff --git a/apps/server/src/modules/system/domain/factory/index.ts b/apps/server/src/modules/system/domain/factory/index.ts deleted file mode 100644 index 1fe32e7e8b0..00000000000 --- a/apps/server/src/modules/system/domain/factory/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './system.factory'; diff --git a/apps/server/src/modules/system/domain/factory/system.factory.spec.ts b/apps/server/src/modules/system/domain/factory/system.factory.spec.ts deleted file mode 100644 index 69285dcb6de..00000000000 --- a/apps/server/src/modules/system/domain/factory/system.factory.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SystemType } from '../system-type.enum'; -import { System, SystemProps } from '../system.do'; -import { SystemFactory } from './system.factory'; - -describe('SystemFactory', () => { - describe('build', () => { - it('should return a system', () => { - const props: SystemProps = { - id: 'id', - type: SystemType.ISERV, - }; - - const result = SystemFactory.build(props); - - expect(result).toBeInstanceOf(System); - }); - }); -}); diff --git a/apps/server/src/modules/system/domain/factory/system.factory.ts b/apps/server/src/modules/system/domain/factory/system.factory.ts deleted file mode 100644 index dfcd31a4bbc..00000000000 --- a/apps/server/src/modules/system/domain/factory/system.factory.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { System, SystemProps } from '../system.do'; - -export class SystemFactory { - static build(props: SystemProps) { - return new System(props); - } -} diff --git a/apps/server/src/modules/system/domain/index.ts b/apps/server/src/modules/system/domain/index.ts index 128d3542fcc..217ea102212 100644 --- a/apps/server/src/modules/system/domain/index.ts +++ b/apps/server/src/modules/system/domain/index.ts @@ -1,6 +1,7 @@ -export { SystemFactory } from './factory/system.factory'; export * from './interface'; +export * from './event'; export { LdapConfig } from './ldap-config'; export { OauthConfig } from './oauth-config'; +export { OidcConfig } from './oidc-config'; export { SystemType } from './system-type.enum'; export { System, SystemProps } from './system.do'; diff --git a/apps/server/src/modules/system/domain/interface/index.ts b/apps/server/src/modules/system/domain/interface/index.ts index 89555b6b881..8e001841153 100644 --- a/apps/server/src/modules/system/domain/interface/index.ts +++ b/apps/server/src/modules/system/domain/interface/index.ts @@ -1 +1,2 @@ -export * from './system.repo.interface'; +export { SystemRepo, SYSTEM_REPO } from './system.repo.interface'; +export { SystemQuery } from './system-query'; diff --git a/apps/server/src/modules/system/domain/interface/system-query.ts b/apps/server/src/modules/system/domain/interface/system-query.ts new file mode 100644 index 00000000000..19c55d844d7 --- /dev/null +++ b/apps/server/src/modules/system/domain/interface/system-query.ts @@ -0,0 +1,5 @@ +import { SystemType } from '../system-type.enum'; + +export interface SystemQuery { + types?: SystemType[]; +} diff --git a/apps/server/src/modules/system/domain/interface/system.repo.interface.ts b/apps/server/src/modules/system/domain/interface/system.repo.interface.ts index 2600c79b0dc..fdfb8bbacb1 100644 --- a/apps/server/src/modules/system/domain/interface/system.repo.interface.ts +++ b/apps/server/src/modules/system/domain/interface/system.repo.interface.ts @@ -1,7 +1,10 @@ import { EntityId } from '@shared/domain/types/entity-id'; import { System } from '../system.do'; +import { SystemQuery } from './system-query'; export interface SystemRepo { + find(filter: SystemQuery): Promise; + getSystemsByIds(systemIds: EntityId[]): Promise; getSystemById(systemId: EntityId): Promise; diff --git a/apps/server/src/modules/system/domain/ldap-config.ts b/apps/server/src/modules/system/domain/ldap-config.ts index 137d1fc92f6..785b41ce35e 100644 --- a/apps/server/src/modules/system/domain/ldap-config.ts +++ b/apps/server/src/modules/system/domain/ldap-config.ts @@ -1,3 +1,5 @@ +import { EntityId } from '@shared/domain/types'; + export class LdapConfig { active: boolean; @@ -5,9 +7,33 @@ export class LdapConfig { provider?: string; + federalState?: EntityId; + + lastSyncAttempt?: Date; + + lastSuccessfulFullSync?: Date; + + lastSuccessfulPartialSync?: Date; + + lastModifyTimestamp?: string; + + rootPath?: string; + + searchUser?: string; + + searchUserPassword?: string; + constructor(props: LdapConfig) { this.active = props.active; this.url = props.url; this.provider = props.provider; + this.federalState = props.federalState; + this.lastSyncAttempt = props.lastSyncAttempt; + this.lastSuccessfulFullSync = props.lastSuccessfulFullSync; + this.lastSuccessfulPartialSync = props.lastSuccessfulPartialSync; + this.lastModifyTimestamp = props.lastModifyTimestamp; + this.rootPath = props.rootPath; + this.searchUser = props.searchUser; + this.searchUserPassword = props.searchUserPassword; } } diff --git a/apps/server/src/modules/system/domain/oidc-config.ts b/apps/server/src/modules/system/domain/oidc-config.ts new file mode 100644 index 00000000000..8d8dbfcc2d9 --- /dev/null +++ b/apps/server/src/modules/system/domain/oidc-config.ts @@ -0,0 +1,28 @@ +export class OidcConfig { + clientId: string; + + clientSecret: string; + + idpHint: string; + + authorizationUrl: string; + + tokenUrl: string; + + logoutUrl: string; + + userinfoUrl: string; + + defaultScopes: string; + + constructor(oauthConfigDto: OidcConfig) { + this.clientId = oauthConfigDto.clientId; + this.clientSecret = oauthConfigDto.clientSecret; + this.idpHint = oauthConfigDto.idpHint; + this.authorizationUrl = oauthConfigDto.authorizationUrl; + this.tokenUrl = oauthConfigDto.tokenUrl; + this.logoutUrl = oauthConfigDto.logoutUrl; + this.userinfoUrl = oauthConfigDto.userinfoUrl; + this.defaultScopes = oauthConfigDto.defaultScopes; + } +} diff --git a/apps/server/src/modules/system/domain/query/index.ts b/apps/server/src/modules/system/domain/query/index.ts deleted file mode 100644 index c8ad01154ee..00000000000 --- a/apps/server/src/modules/system/domain/query/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './system-query'; diff --git a/apps/server/src/modules/system/domain/query/system-query.ts b/apps/server/src/modules/system/domain/query/system-query.ts deleted file mode 100644 index 677b2f160f4..00000000000 --- a/apps/server/src/modules/system/domain/query/system-query.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityId } from '@shared/domain/types/entity-id'; - -export interface SystemQuery { - ids?: EntityId[]; -} diff --git a/apps/server/src/modules/system/domain/system.do.ts b/apps/server/src/modules/system/domain/system.do.ts index 51b3e92d2c0..6255cdd32be 100644 --- a/apps/server/src/modules/system/domain/system.do.ts +++ b/apps/server/src/modules/system/domain/system.do.ts @@ -2,6 +2,7 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { LdapConfig } from './ldap-config'; import { OauthConfig } from './oauth-config'; +import { OidcConfig } from './oidc-config'; import { SystemType } from './system-type.enum'; export interface SystemProps extends AuthorizableObject { @@ -20,6 +21,8 @@ export interface SystemProps extends AuthorizableObject { oauthConfig?: OauthConfig; ldapConfig?: LdapConfig; + + oidcConfig?: OidcConfig; } export class System extends DomainObject { @@ -27,6 +30,14 @@ export class System extends DomainObject { return this.props.type; } + get alias(): string | undefined { + return this.props.alias; + } + + get displayName(): string | undefined { + return this.props.displayName; + } + get ldapConfig(): LdapConfig | undefined { return this.props.ldapConfig; } @@ -35,6 +46,18 @@ export class System extends DomainObject { return this.props.provisioningStrategy; } + get provisioningUrl(): string | undefined { + return this.props.provisioningUrl; + } + + get oauthConfig(): OauthConfig | undefined { + return this.props.oauthConfig; + } + + get oidcConfig(): OidcConfig | undefined { + return this.props.oidcConfig; + } + public isDeletable(): boolean { const isDeletable = this.ldapConfig?.provider === 'general'; diff --git a/apps/server/src/modules/system/entity/index.ts b/apps/server/src/modules/system/entity/index.ts new file mode 100644 index 00000000000..ad6f6138a38 --- /dev/null +++ b/apps/server/src/modules/system/entity/index.ts @@ -0,0 +1,7 @@ +export { + SystemEntity, + SystemEntityProps, + LdapConfigEntity, + OauthConfigEntity, + OidcConfigEntity, +} from './system.entity'; diff --git a/apps/server/src/shared/domain/entity/system.entity.spec.ts b/apps/server/src/modules/system/entity/system.entity.spec.ts similarity index 100% rename from apps/server/src/shared/domain/entity/system.entity.spec.ts rename to apps/server/src/modules/system/entity/system.entity.spec.ts diff --git a/apps/server/src/shared/domain/entity/system.entity.ts b/apps/server/src/modules/system/entity/system.entity.ts similarity index 97% rename from apps/server/src/shared/domain/entity/system.entity.ts rename to apps/server/src/modules/system/entity/system.entity.ts index 35b84b8bcf6..d86be69e7b1 100644 --- a/apps/server/src/shared/domain/entity/system.entity.ts +++ b/apps/server/src/modules/system/entity/system.entity.ts @@ -1,8 +1,8 @@ import { Cascade, Collection, Embeddable, Embedded, Entity, Enum, OneToMany, Property } from '@mikro-orm/core'; import { SchoolSystemOptionsEntity } from '@modules/legacy-school/entity'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { EntityId } from '../types'; -import { BaseEntityWithTimestamps } from './base.entity'; +import { EntityId } from '@shared/domain/types'; export interface SystemEntityProps { type: string; diff --git a/apps/server/src/modules/system/index.ts b/apps/server/src/modules/system/index.ts index 5789bd88f84..e391f13df16 100644 --- a/apps/server/src/modules/system/index.ts +++ b/apps/server/src/modules/system/index.ts @@ -1,3 +1,13 @@ -export { LdapConfig, OauthConfig, System, SystemProps } from './domain'; -export { LegacySystemService, OauthConfigDto, OidcConfigDto, SystemDto, SystemService } from './service'; +export { + LdapConfig, + OauthConfig, + OidcConfig, + System, + SystemProps, + SYSTEM_REPO, + SystemRepo, + SystemType, + SystemDeletedEvent, +} from './domain'; +export { SystemService } from './service'; export { SystemModule } from './system.module'; diff --git a/apps/server/src/modules/system/mapper/index.ts b/apps/server/src/modules/system/mapper/index.ts deleted file mode 100644 index 9dc568e504c..00000000000 --- a/apps/server/src/modules/system/mapper/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SystemMapper } from './system.mapper'; -export { SystemOidcMapper } from './system-oidc.mapper'; diff --git a/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts b/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts deleted file mode 100644 index 49c78b4dc2f..00000000000 --- a/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SystemEntity } from '@shared/domain/entity'; -import { systemEntityFactory } from '@shared/testing'; -import { SystemOidcMapper } from './system-oidc.mapper'; - -describe('SystemOidcMapper', () => { - let module: TestingModule; - afterAll(async () => { - await module.close(); - }); - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [], - providers: [SystemOidcMapper], - }).compile(); - }); - - describe('mapFromEntityToDto', () => { - it('should map all fields', () => { - const systemEntity = systemEntityFactory.withOauthConfig().withOidcConfig().build(); - - const result = SystemOidcMapper.mapFromEntityToDto(systemEntity); - expect(result).toBeDefined(); - - expect(result?.parentSystemId).toEqual(systemEntity.id); - expect(result?.clientId).toEqual(systemEntity.oidcConfig?.clientId); - expect(result?.clientSecret).toEqual(systemEntity.oidcConfig?.clientSecret); - expect(result?.idpHint).toEqual(systemEntity.oidcConfig?.idpHint); - expect(result?.authorizationUrl).toEqual(systemEntity.oidcConfig?.authorizationUrl); - expect(result?.tokenUrl).toEqual(systemEntity.oidcConfig?.tokenUrl); - expect(result?.userinfoUrl).toEqual(systemEntity.oidcConfig?.userinfoUrl); - expect(result?.logoutUrl).toEqual(systemEntity.oidcConfig?.logoutUrl); - expect(result?.defaultScopes).toEqual(systemEntity.oidcConfig?.defaultScopes); - }); - it('should return undefined if parent system has no oidc config', () => { - const systemEntity = systemEntityFactory.withOauthConfig().build(); - const result = SystemOidcMapper.mapFromEntityToDto(systemEntity); - expect(result).toBeUndefined(); - }); - }); - - describe('mapFromEntitiesToDtos', () => { - it('should map all given entities', () => { - const systemEntities: SystemEntity[] = [ - systemEntityFactory.withOidcConfig().build(), - systemEntityFactory.withOidcConfig().build(), - ]; - - const result = SystemOidcMapper.mapFromEntitiesToDtos(systemEntities); - - expect(result.length).toBe(systemEntities.length); - }); - - it('should map oidc config only config if exists', () => { - const systemEntity = systemEntityFactory.withOidcConfig().build(); - const systemEntities: SystemEntity[] = [systemEntity, systemEntityFactory.withOauthConfig().build()]; - - const results = SystemOidcMapper.mapFromEntitiesToDtos(systemEntities); - - expect(results.length).toBe(1); - - const [theResult] = results; - - expect(theResult.parentSystemId).toEqual(systemEntity.id); - expect(theResult.clientId).toEqual(systemEntity.oidcConfig?.clientId); - expect(theResult.clientSecret).toEqual(systemEntity.oidcConfig?.clientSecret); - expect(theResult.idpHint).toEqual(systemEntity.oidcConfig?.idpHint); - expect(theResult.authorizationUrl).toEqual(systemEntity.oidcConfig?.authorizationUrl); - expect(theResult.tokenUrl).toEqual(systemEntity.oidcConfig?.tokenUrl); - expect(theResult.userinfoUrl).toEqual(systemEntity.oidcConfig?.userinfoUrl); - expect(theResult.logoutUrl).toEqual(systemEntity.oidcConfig?.logoutUrl); - expect(theResult.defaultScopes).toEqual(systemEntity.oidcConfig?.defaultScopes); - }); - }); -}); diff --git a/apps/server/src/modules/system/mapper/system-oidc.mapper.ts b/apps/server/src/modules/system/mapper/system-oidc.mapper.ts deleted file mode 100644 index e1a2477e2a6..00000000000 --- a/apps/server/src/modules/system/mapper/system-oidc.mapper.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { OidcConfigDto } from '@modules/system/service/dto/oidc-config.dto'; -import { OidcConfigEntity, SystemEntity } from '@shared/domain/entity'; - -export class SystemOidcMapper { - static mapFromEntityToDto(entity: SystemEntity): OidcConfigDto | undefined { - if (entity.oidcConfig) { - return SystemOidcMapper.mapFromOidcConfigEntityToDto(entity.id, entity.oidcConfig); - } - return undefined; - } - - static mapFromOidcConfigEntityToDto(systemId: string, oidcConfig: OidcConfigEntity): OidcConfigDto { - return new OidcConfigDto({ - parentSystemId: systemId, - clientId: oidcConfig.clientId, - clientSecret: oidcConfig?.clientSecret, - idpHint: oidcConfig.idpHint, - authorizationUrl: oidcConfig.authorizationUrl, - tokenUrl: oidcConfig.tokenUrl, - userinfoUrl: oidcConfig.userinfoUrl, - logoutUrl: oidcConfig.logoutUrl, - defaultScopes: oidcConfig.defaultScopes, - }); - } - - static mapFromEntitiesToDtos(entities: SystemEntity[]): OidcConfigDto[] { - return entities - .map((entity) => this.mapFromEntityToDto(entity)) - .filter((entity): entity is OidcConfigDto => entity !== undefined); - } -} diff --git a/apps/server/src/modules/system/mapper/system.mapper.spec.ts b/apps/server/src/modules/system/mapper/system.mapper.spec.ts deleted file mode 100644 index 02af426d04c..00000000000 --- a/apps/server/src/modules/system/mapper/system.mapper.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SystemEntity } from '@shared/domain/entity'; -import { systemEntityFactory } from '@shared/testing'; -import { SystemMapper } from './system.mapper'; - -describe('SystemMapper', () => { - let module: TestingModule; - afterAll(async () => { - await module.close(); - }); - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [], - providers: [SystemMapper], - }).compile(); - }); - - describe('mapFromEntityToDto', () => { - it('should map all fields', () => { - const systemEntity = systemEntityFactory.withOauthConfig().withOidcConfig().build(); - - const result = SystemMapper.mapFromEntityToDto(systemEntity); - - expect(result.url).toEqual(systemEntity.url); - expect(result.alias).toEqual(systemEntity.alias); - expect(result.displayName).toEqual(systemEntity.displayName); - expect(result.type).toEqual(systemEntity.type); - expect(result.provisioningStrategy).toEqual(systemEntity.provisioningStrategy); - expect(result.provisioningUrl).toEqual(systemEntity.provisioningUrl); - expect(result.oauthConfig).toEqual(systemEntity.oauthConfig); - }); - it('should map take alias as default instead of displayName', () => { - // Arrange - const systemEntity = systemEntityFactory.withOauthConfig().build(); - systemEntity.displayName = undefined; - - // Act - const result = SystemMapper.mapFromEntityToDto(systemEntity); - - // Assert - expect(result.alias).toEqual(systemEntity.alias); - expect(result.displayName).toEqual(systemEntity.alias); - }); - }); - - describe('mapFromEntitiesToDtos', () => { - it('should map all given entities', () => { - const systemEntities: SystemEntity[] = [ - systemEntityFactory.withOauthConfig().build(), - systemEntityFactory.build({ oauthConfig: undefined }), - ]; - - const result = SystemMapper.mapFromEntitiesToDtos(systemEntities); - - expect(result.length).toBe(systemEntities.length); - }); - - it('should map oauth config if exists', () => { - const systemEntities: SystemEntity[] = [ - systemEntityFactory.withOauthConfig().build(), - systemEntityFactory.build({ oauthConfig: undefined }), - ]; - - const result = SystemMapper.mapFromEntitiesToDtos(systemEntities); - - expect(result[0].oauthConfig?.clientId).toEqual(systemEntities[0].oauthConfig?.clientId); - expect(result[0].oauthConfig?.clientSecret).toEqual(systemEntities[0].oauthConfig?.clientSecret); - expect(result[0].oauthConfig?.grantType).toEqual(systemEntities[0].oauthConfig?.grantType); - expect(result[0].oauthConfig?.tokenEndpoint).toEqual(systemEntities[0].oauthConfig?.tokenEndpoint); - expect(result[0].oauthConfig?.authEndpoint).toEqual(systemEntities[0].oauthConfig?.authEndpoint); - expect(result[0].oauthConfig?.responseType).toEqual(systemEntities[0].oauthConfig?.responseType); - expect(result[0].oauthConfig?.scope).toEqual(systemEntities[0].oauthConfig?.scope); - expect(result[0].oauthConfig?.provider).toEqual(systemEntities[0].oauthConfig?.provider); - expect(result[0].oauthConfig?.logoutEndpoint).toEqual(systemEntities[0].oauthConfig?.logoutEndpoint); - expect(result[0].oauthConfig?.issuer).toEqual(systemEntities[0].oauthConfig?.issuer); - expect(result[0].oauthConfig?.jwksEndpoint).toEqual(systemEntities[0].oauthConfig?.jwksEndpoint); - expect(result[0].oauthConfig?.redirectUri).toEqual(systemEntities[0].oauthConfig?.redirectUri); - expect(result[1].oauthConfig).toBe(undefined); - }); - }); -}); diff --git a/apps/server/src/modules/system/mapper/system.mapper.ts b/apps/server/src/modules/system/mapper/system.mapper.ts deleted file mode 100644 index 2837d8eb912..00000000000 --- a/apps/server/src/modules/system/mapper/system.mapper.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { OauthConfigEntity, SystemEntity } from '@shared/domain/entity'; - -export class SystemMapper { - static mapFromEntityToDto(entity: SystemEntity): SystemDto { - return new SystemDto({ - id: entity.id, - type: entity.type, - url: entity.url, - alias: entity.alias, - displayName: entity.displayName ?? entity.alias, - provisioningStrategy: entity.provisioningStrategy, - provisioningUrl: entity.provisioningUrl, - oauthConfig: SystemMapper.mapFromOauthConfigEntityToDto(entity.oauthConfig), - ldapActive: entity.ldapConfig?.active, - }); - } - - static mapFromOauthConfigEntityToDto(oauthConfig: OauthConfigEntity | undefined): OauthConfigDto | undefined { - if (!oauthConfig) return undefined; - return new OauthConfigDto({ - clientId: oauthConfig.clientId, - clientSecret: oauthConfig.clientSecret, - idpHint: oauthConfig.idpHint, - redirectUri: oauthConfig.redirectUri, - grantType: oauthConfig.grantType, - tokenEndpoint: oauthConfig.tokenEndpoint, - authEndpoint: oauthConfig.authEndpoint, - responseType: oauthConfig.responseType, - scope: oauthConfig.scope, - provider: oauthConfig.provider, - logoutEndpoint: oauthConfig.logoutEndpoint, - issuer: oauthConfig.issuer, - jwksEndpoint: oauthConfig.jwksEndpoint, - }); - } - - static mapFromEntitiesToDtos(entities: SystemEntity[]): SystemDto[] { - return entities.map((entity) => this.mapFromEntityToDto(entity)); - } -} diff --git a/apps/server/src/modules/system/repo/mikro-orm/mapper/system-entity.mapper.ts b/apps/server/src/modules/system/repo/mikro-orm/mapper/system-entity.mapper.ts index 13fe03437e2..a8da1afa1b2 100644 --- a/apps/server/src/modules/system/repo/mikro-orm/mapper/system-entity.mapper.ts +++ b/apps/server/src/modules/system/repo/mikro-orm/mapper/system-entity.mapper.ts @@ -1,9 +1,9 @@ -import { LdapConfigEntity, OauthConfigEntity, SystemEntity } from '@shared/domain/entity'; -import { LdapConfig, OauthConfig, System, SystemFactory } from '../../../domain'; +import { LdapConfig, OauthConfig, OidcConfig, System } from '../../../domain'; +import { LdapConfigEntity, OauthConfigEntity, OidcConfigEntity, SystemEntity } from '../../../entity'; export class SystemEntityMapper { public static mapToDo(entity: SystemEntity): System { - const system = SystemFactory.build({ + const system = new System({ id: entity.id, url: entity.url, type: entity.type, @@ -13,6 +13,7 @@ export class SystemEntityMapper { alias: entity.alias, oauthConfig: entity.oauthConfig ? this.mapOauthConfigEntityToDomainObject(entity.oauthConfig) : undefined, ldapConfig: entity.ldapConfig ? this.mapLdapConfigEntityToDomainObject(entity.ldapConfig) : undefined, + oidcConfig: entity.oidcConfig ? this.mapOidcConfigEntityToDomainObject(entity.oidcConfig) : undefined, }); return system; @@ -43,6 +44,29 @@ export class SystemEntityMapper { active: !!ldapConfig.active, url: ldapConfig.url, provider: ldapConfig.provider, + federalState: ldapConfig.federalState, + lastSyncAttempt: ldapConfig.lastSyncAttempt, + lastSuccessfulFullSync: ldapConfig.lastSuccessfulFullSync, + lastSuccessfulPartialSync: ldapConfig.lastSuccessfulPartialSync, + lastModifyTimestamp: ldapConfig.lastModifyTimestamp, + rootPath: ldapConfig.rootPath, + searchUser: ldapConfig.searchUser, + searchUserPassword: ldapConfig.searchUserPassword, + }); + + return mapped; + } + + private static mapOidcConfigEntityToDomainObject(oidcConfig: OidcConfigEntity): OidcConfig { + const mapped: OidcConfig = new OidcConfig({ + clientId: oidcConfig.clientId, + clientSecret: oidcConfig?.clientSecret, + idpHint: oidcConfig.idpHint, + authorizationUrl: oidcConfig.authorizationUrl, + tokenUrl: oidcConfig.tokenUrl, + userinfoUrl: oidcConfig.userinfoUrl, + logoutUrl: oidcConfig.logoutUrl, + defaultScopes: oidcConfig.defaultScopes, }); return mapped; diff --git a/apps/server/src/modules/system/repo/mikro-orm/scope/index.ts b/apps/server/src/modules/system/repo/mikro-orm/scope/index.ts new file mode 100644 index 00000000000..583ab6797bb --- /dev/null +++ b/apps/server/src/modules/system/repo/mikro-orm/scope/index.ts @@ -0,0 +1 @@ +export { SystemScope } from './system.scope'; diff --git a/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.spec.ts b/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.spec.ts new file mode 100644 index 00000000000..f44eab7b409 --- /dev/null +++ b/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.spec.ts @@ -0,0 +1,46 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { EmptyResultQuery } from '@shared/repo/query'; +import { SystemType } from '../../../domain'; +import { SystemScope } from './system.scope'; + +describe(SystemScope.name, () => { + describe('byIds', () => { + describe('when filtering by ids', () => { + it('should have a query for ids', () => { + const ids = [new ObjectId().toHexString(), new ObjectId().toHexString()]; + + const scope = new SystemScope().byIds(ids); + + expect(scope.query).toEqual({ id: { $in: ids } }); + }); + }); + + describe('when not providing ids', () => { + it('should not add a query', () => { + const scope = new SystemScope().byIds(undefined); + + expect(scope.query).toEqual(EmptyResultQuery); + }); + }); + }); + + describe('byTypes', () => { + describe('when filtering by types', () => { + it('should have a query for types', () => { + const types = [SystemType.LDAP, SystemType.OAUTH]; + + const scope = new SystemScope().byTypes(types); + + expect(scope.query).toEqual({ type: { $in: types } }); + }); + }); + + describe('when not providing types', () => { + it('should not add a query', () => { + const scope = new SystemScope().byTypes(undefined); + + expect(scope.query).toEqual(EmptyResultQuery); + }); + }); + }); +}); diff --git a/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.ts b/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.ts index 073d677e322..c59fc997d05 100644 --- a/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.ts +++ b/apps/server/src/modules/system/repo/mikro-orm/scope/system.scope.ts @@ -1,11 +1,22 @@ -import { SystemEntity } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types/entity-id'; import { Scope } from '@shared/repo/scope'; +import { SystemType } from '../../../domain'; +import { SystemEntity } from '../../../entity'; export class SystemScope extends Scope { - byIds(ids?: EntityId[]) { + byIds(ids?: EntityId[]): this { if (ids) { this.addQuery({ id: { $in: ids } }); } + + return this; + } + + byTypes(types?: SystemType[]): this { + if (types) { + this.addQuery({ type: { $in: types } }); + } + + return this; } } diff --git a/apps/server/src/modules/system/repo/mikro-orm/system.repo.spec.ts b/apps/server/src/modules/system/repo/mikro-orm/system.repo.spec.ts index 157930f04fe..ba3ae03afe3 100644 --- a/apps/server/src/modules/system/repo/mikro-orm/system.repo.spec.ts +++ b/apps/server/src/modules/system/repo/mikro-orm/system.repo.spec.ts @@ -2,12 +2,18 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LdapConfigEntity, OauthConfigEntity, SystemEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { SystemTypeEnum } from '@shared/domain/types'; -import { cleanupCollections, systemEntityFactory } from '@shared/testing'; -import { SYSTEM_REPO, System, SystemProps, SystemRepo } from '../../domain'; -import { SystemEntityMapper } from './mapper/system-entity.mapper'; +import { + cleanupCollections, + systemEntityFactory, + systemLdapConfigFactory, + systemOauthConfigFactory, + systemOidcConfigFactory, +} from '@shared/testing'; +import { System, SYSTEM_REPO, SystemProps, SystemRepo, SystemType } from '../../domain'; +import { SystemEntity } from '../../entity'; +import { SystemEntityMapper } from './mapper'; import { SystemMikroOrmRepo } from './system.repo'; describe(SystemMikroOrmRepo.name, () => { @@ -33,29 +39,79 @@ describe(SystemMikroOrmRepo.name, () => { await cleanupCollections(em); }); + describe('find', () => { + describe('when no filter is provided', () => { + const setup = async () => { + const ldapSystem = systemEntityFactory.buildWithId({ type: SystemType.LDAP }); + const oauthSystem = systemEntityFactory.buildWithId({ type: SystemType.OAUTH }); + + await em.persistAndFlush([ldapSystem, oauthSystem]); + em.clear(); + + return { + ldapSystem, + oauthSystem, + }; + }; + + it('should return all systems', async () => { + const { ldapSystem, oauthSystem } = await setup(); + + const result = await repo.find({}); + + expect(result).toEqual([SystemEntityMapper.mapToDo(ldapSystem), SystemEntityMapper.mapToDo(oauthSystem)]); + }); + }); + + describe('when no system matches the filter', () => { + const setup = async () => { + const ldapSystem = systemEntityFactory.buildWithId({ type: SystemType.LDAP }); + + await em.persistAndFlush([ldapSystem]); + em.clear(); + }; + + it('should return an empty array', async () => { + await setup(); + + const result = await repo.find({ types: [SystemType.OAUTH] }); + + expect(result).toEqual([]); + }); + }); + + describe('when a system matches the filter', () => { + const setup = async () => { + const ldapSystem = systemEntityFactory.buildWithId({ type: SystemType.LDAP }); + const oauthSystem1 = systemEntityFactory.buildWithId({ type: SystemType.OAUTH }); + const oauthSystem2 = systemEntityFactory.buildWithId({ type: SystemType.OAUTH }); + + await em.persistAndFlush([ldapSystem, oauthSystem1, oauthSystem2]); + em.clear(); + + return { + oauthSystem1, + oauthSystem2, + }; + }; + + it('should return the systems', async () => { + const { oauthSystem1, oauthSystem2 } = await setup(); + + const result = await repo.find({ types: [SystemType.OAUTH] }); + + expect(result).toEqual([SystemEntityMapper.mapToDo(oauthSystem1), SystemEntityMapper.mapToDo(oauthSystem2)]); + }); + }); + }); + describe('getSystemById', () => { describe('when the system exists', () => { const setup = async () => { - const oauthConfig = new OauthConfigEntity({ - clientId: '12345', - clientSecret: 'mocksecret', - idpHint: 'mock-oauth-idpHint', - tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', - grantType: 'authorization_code', - redirectUri: 'http://mockhost:3030/api/v3/sso/oauth/', - 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 ldapConfig = new LdapConfigEntity({ - url: 'ldaps:mock.de:389', - active: true, - provider: 'mock_provider', - }); + const oauthConfig = systemOauthConfigFactory.build(); + const ldapConfig = systemLdapConfigFactory.build(); + const oidcConfig = systemOidcConfigFactory.build(); + const system: SystemEntity = systemEntityFactory.buildWithId({ type: 'oauth', url: 'https://mock.de', @@ -65,6 +121,7 @@ describe(SystemMikroOrmRepo.name, () => { provisioningUrl: 'https://provisioningurl.de', oauthConfig, ldapConfig, + oidcConfig, }); await em.persistAndFlush([system]); @@ -74,11 +131,12 @@ describe(SystemMikroOrmRepo.name, () => { system, oauthConfig, ldapConfig, + oidcConfig, }; }; it('should return the system', async () => { - const { system, oauthConfig, ldapConfig } = await setup(); + const { system, oauthConfig, ldapConfig, oidcConfig } = await setup(); const result = await repo.getSystemById(system.id); @@ -110,6 +168,16 @@ describe(SystemMikroOrmRepo.name, () => { provider: ldapConfig.provider, active: !!ldapConfig.active, }, + oidcConfig: { + clientId: oidcConfig.clientId, + clientSecret: oidcConfig.clientSecret, + idpHint: oidcConfig.idpHint, + authorizationUrl: oidcConfig.authorizationUrl, + tokenUrl: oidcConfig.tokenUrl, + logoutUrl: oidcConfig.logoutUrl, + userinfoUrl: oidcConfig.userinfoUrl, + defaultScopes: oidcConfig.defaultScopes, + }, }); }); }); diff --git a/apps/server/src/modules/system/repo/mikro-orm/system.repo.ts b/apps/server/src/modules/system/repo/mikro-orm/system.repo.ts index cfd64f8dd66..c83d82abcbf 100644 --- a/apps/server/src/modules/system/repo/mikro-orm/system.repo.ts +++ b/apps/server/src/modules/system/repo/mikro-orm/system.repo.ts @@ -1,11 +1,11 @@ import { EntityData, EntityName } from '@mikro-orm/core'; import { Injectable, NotImplementedException } from '@nestjs/common'; -import { SystemEntity } from '@shared/domain/entity'; import { EntityId, SystemTypeEnum } from '@shared/domain/types'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; -import { System, SystemRepo } from '../../domain'; -import { SystemEntityMapper } from './mapper/system-entity.mapper'; -import { SystemScope } from './scope/system.scope'; +import { System, SystemQuery, SystemRepo } from '../../domain'; +import { SystemEntity } from '../../entity'; +import { SystemEntityMapper } from './mapper'; +import { SystemScope } from './scope'; @Injectable() export class SystemMikroOrmRepo extends BaseDomainObjectRepo implements SystemRepo { @@ -17,6 +17,16 @@ export class SystemMikroOrmRepo extends BaseDomainObjectRepo { + const scope: SystemScope = new SystemScope().byTypes(filter.types).allowEmptyQuery(true); + + const entities: SystemEntity[] = await this.em.find(SystemEntity, scope.query); + + const domainObjects: System[] = entities.map((entity: SystemEntity) => SystemEntityMapper.mapToDo(entity)); + + return domainObjects; + } + public async getSystemById(id: EntityId): Promise { const entity: SystemEntity | null = await this.em.findOne(SystemEntity, { id }); @@ -41,7 +51,6 @@ export class SystemMikroOrmRepo extends BaseDomainObjectRepo { - // Systems with an oauthConfig are filtered out here to exclude IServ. IServ is of type LDAP for syncing purposes, but the login is done via OAuth2. const entities: SystemEntity[] = await this.em.find(SystemEntity, { type: SystemTypeEnum.LDAP, ldapConfig: { active: true }, diff --git a/apps/server/src/modules/system/service/dto/index.ts b/apps/server/src/modules/system/service/dto/index.ts deleted file mode 100644 index c0a8942caa6..00000000000 --- a/apps/server/src/modules/system/service/dto/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './oauth-config.dto'; -export * from './oidc-config.dto'; -export * from './system.dto'; diff --git a/apps/server/src/modules/system/service/dto/oauth-config.dto.ts b/apps/server/src/modules/system/service/dto/oauth-config.dto.ts deleted file mode 100644 index 7af97200971..00000000000 --- a/apps/server/src/modules/system/service/dto/oauth-config.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -export class OauthConfigDto { - clientId: string; - - clientSecret: string; - - idpHint?: string; - - redirectUri: string; - - grantType: string; - - tokenEndpoint: string; - - authEndpoint: string; - - responseType: string; - - scope: string; - - provider: string; - - /** - * If this is set it will be used to redirect the user after login to the logout endpoint of the identity provider. - */ - logoutEndpoint?: string; - - issuer: string; - - jwksEndpoint: string; - - constructor(oauthConfigDto: OauthConfigDto) { - this.clientId = oauthConfigDto.clientId; - this.clientSecret = oauthConfigDto.clientSecret; - this.idpHint = oauthConfigDto.idpHint; - this.redirectUri = oauthConfigDto.redirectUri; - this.grantType = oauthConfigDto.grantType; - this.tokenEndpoint = oauthConfigDto.tokenEndpoint; - this.authEndpoint = oauthConfigDto.authEndpoint; - this.responseType = oauthConfigDto.responseType; - this.scope = oauthConfigDto.scope; - this.provider = oauthConfigDto.provider; - this.logoutEndpoint = oauthConfigDto.logoutEndpoint; - this.issuer = oauthConfigDto.issuer; - this.jwksEndpoint = oauthConfigDto.jwksEndpoint; - } -} diff --git a/apps/server/src/modules/system/service/dto/oidc-config.dto.ts b/apps/server/src/modules/system/service/dto/oidc-config.dto.ts deleted file mode 100644 index 41873fd3fed..00000000000 --- a/apps/server/src/modules/system/service/dto/oidc-config.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -export class OidcConfigDto { - constructor(oidcConfigDto: OidcConfigDto) { - this.parentSystemId = oidcConfigDto.parentSystemId; - this.clientId = oidcConfigDto.clientId; - this.clientSecret = oidcConfigDto.clientSecret; - this.idpHint = oidcConfigDto.idpHint; - this.authorizationUrl = oidcConfigDto.authorizationUrl; - this.tokenUrl = oidcConfigDto.tokenUrl; - this.userinfoUrl = oidcConfigDto.userinfoUrl; - this.logoutUrl = oidcConfigDto.logoutUrl; - this.defaultScopes = oidcConfigDto.defaultScopes; - } - - parentSystemId: string; - - clientId: string; - - clientSecret: string; - - idpHint: string; - - authorizationUrl: string; - - tokenUrl: string; - - logoutUrl: string; - - userinfoUrl: string; - - defaultScopes: string; -} diff --git a/apps/server/src/modules/system/service/dto/system.dto.ts b/apps/server/src/modules/system/service/dto/system.dto.ts deleted file mode 100644 index bdffdf75423..00000000000 --- a/apps/server/src/modules/system/service/dto/system.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { EntityId } from '@shared/domain/types'; - -export class SystemDto { - id?: EntityId; - - type: string; - - url?: string; - - alias?: string; - - displayName?: string; - - provisioningStrategy?: SystemProvisioningStrategy; - - provisioningUrl?: string; - - oauthConfig?: OauthConfigDto; - - ldapActive?: boolean; - - constructor(system: SystemDto) { - this.id = system.id; - this.type = system.type; - this.url = system.url; - this.alias = system.alias; - this.displayName = system.displayName; - this.provisioningStrategy = system.provisioningStrategy; - this.provisioningUrl = system.provisioningUrl; - this.oauthConfig = system.oauthConfig; - this.ldapActive = system.ldapActive; - } -} diff --git a/apps/server/src/modules/system/service/index.ts b/apps/server/src/modules/system/service/index.ts index 6be1d3fb0fa..3bc23ee212e 100644 --- a/apps/server/src/modules/system/service/index.ts +++ b/apps/server/src/modules/system/service/index.ts @@ -1,4 +1 @@ -export { LegacySystemService } from './legacy-system.service'; -export { SystemDto, OauthConfigDto, OidcConfigDto } from './dto'; export { SystemService } from './system.service'; -export { SystemOidcService } from './system-oidc.service'; diff --git a/apps/server/src/modules/system/service/legacy-system.service.spec.ts b/apps/server/src/modules/system/service/legacy-system.service.spec.ts deleted file mode 100644 index c1528da8ead..00000000000 --- a/apps/server/src/modules/system/service/legacy-system.service.spec.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { IdentityManagementOauthService } from '@infra/identity-management'; -import { Test, TestingModule } from '@nestjs/testing'; -import { EntityNotFoundError } from '@shared/common'; -import { OauthConfigEntity, SystemEntity } from '@shared/domain/entity'; -import { SystemTypeEnum } from '@shared/domain/types'; -import { LegacySystemRepo } from '@shared/repo'; -import { systemEntityFactory } from '@shared/testing'; -import { SystemMapper } from '../mapper'; -import { LegacySystemService } from './legacy-system.service'; - -describe(LegacySystemService.name, () => { - let module: TestingModule; - let systemService: LegacySystemService; - let systemRepoMock: DeepMocked; - let kcIdmOauthServiceMock: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - LegacySystemService, - { - provide: LegacySystemRepo, - useValue: createMock(), - }, - { - provide: IdentityManagementOauthService, - useValue: createMock(), - }, - ], - }).compile(); - systemRepoMock = module.get(LegacySystemRepo); - systemService = module.get(LegacySystemService); - kcIdmOauthServiceMock = module.get(IdentityManagementOauthService); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('findById', () => { - describe('when identity management is available', () => { - const standaloneSystem = systemEntityFactory.buildWithId({ alias: 'standaloneSystem' }); - const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); - const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); - const setup = (system: SystemEntity) => { - systemRepoMock.findById.mockResolvedValue(system); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(true); - kcIdmOauthServiceMock.getOauthConfig.mockResolvedValue(oauthSystem.oauthConfig as OauthConfigEntity); - }; - - it('should return found system', async () => { - setup(standaloneSystem); - const result = await systemService.findById(standaloneSystem.id); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(standaloneSystem)); - }); - - it('should return found system with generated oauth config for oidc systems', async () => { - setup(oidcSystem); - if (oauthSystem.oauthConfig === undefined) { - fail('oauth system has no oauth configuration'); - } - const result = await systemService.findById(oidcSystem.id); - expect(result).toEqual( - expect.objectContaining({ - id: oidcSystem.id, - type: SystemTypeEnum.OAUTH, - alias: oidcSystem.alias, - displayName: oidcSystem.displayName, - url: oidcSystem.url, - provisioningStrategy: oidcSystem.provisioningStrategy, - provisioningUrl: oidcSystem.provisioningUrl, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - oauthConfig: expect.objectContaining({ - clientId: oauthSystem.oauthConfig.clientId, - clientSecret: oauthSystem.oauthConfig.clientSecret, - idpHint: oidcSystem.oidcConfig?.idpHint, - redirectUri: oauthSystem.oauthConfig.redirectUri + oidcSystem.id, - grantType: oauthSystem.oauthConfig.grantType, - tokenEndpoint: oauthSystem.oauthConfig.tokenEndpoint, - authEndpoint: oauthSystem.oauthConfig.authEndpoint, - responseType: oauthSystem.oauthConfig.responseType, - scope: oauthSystem.oauthConfig.scope, - provider: oauthSystem.oauthConfig.provider, - logoutEndpoint: oauthSystem.oauthConfig.logoutEndpoint, - issuer: oauthSystem.oauthConfig.issuer, - jwksEndpoint: oauthSystem.oauthConfig.jwksEndpoint, - }), - }) - ); - }); - }); - - describe('when identity management is not available', () => { - const standaloneSystem = systemEntityFactory.buildWithId(); - const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId(); - const setup = (system: SystemEntity) => { - systemRepoMock.findById.mockResolvedValue(system); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(false); - }; - - it('should return found system', async () => { - setup(standaloneSystem); - const result = await systemService.findById(standaloneSystem.id); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(standaloneSystem)); - }); - - it('should throw and not generate oauth config for oidc systems', async () => { - setup(oidcSystem); - await expect(systemService.findById(oidcSystem.id)).rejects.toThrow(EntityNotFoundError); - }); - }); - }); - - describe('findByType', () => { - describe('when identity management is available', () => { - const ldapSystem = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'ldapSystem' }); - const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); - const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); - const setup = () => { - systemRepoMock.findAll.mockResolvedValue([ldapSystem, oauthSystem, oidcSystem]); - systemRepoMock.findByFilter.mockImplementation((type: SystemTypeEnum) => { - if (type === SystemTypeEnum.LDAP) return Promise.resolve([ldapSystem]); - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([oauthSystem]); - if (type === SystemTypeEnum.OIDC) return Promise.resolve([oidcSystem]); - return Promise.resolve([]); - }); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(true); - kcIdmOauthServiceMock.getOauthConfig.mockResolvedValue(oauthSystem.oauthConfig as OauthConfigEntity); - }; - - it('should return all systems', async () => { - setup(); - const result = await systemService.findByType(); - expect(result).toEqual( - expect.arrayContaining([ - ...SystemMapper.mapFromEntitiesToDtos([ldapSystem, oauthSystem]), - expect.objectContaining({ - alias: oidcSystem.alias, - displayName: oidcSystem.displayName, - }), - ]) - ); - }); - - it('should return found systems', async () => { - setup(); - const result = await systemService.findByType(SystemTypeEnum.LDAP); - expect(result).toStrictEqual(SystemMapper.mapFromEntitiesToDtos([ldapSystem])); - }); - - it('should return found systems with generated oauth config for oidc systems', async () => { - setup(); - if (oauthSystem.oauthConfig === undefined) { - fail('oauth system has no oauth configuration'); - } - const resultingSystems = await systemService.findByType(SystemTypeEnum.OAUTH); - - expect(resultingSystems).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: SystemTypeEnum.OAUTH, - alias: oidcSystem.alias, - displayName: oidcSystem.displayName, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - oauthConfig: expect.objectContaining({ - clientId: oauthSystem.oauthConfig.clientId, - clientSecret: oauthSystem.oauthConfig.clientSecret, - idpHint: oidcSystem.oidcConfig?.idpHint, - redirectUri: oauthSystem.oauthConfig.redirectUri + oidcSystem.id, - grantType: oauthSystem.oauthConfig.grantType, - tokenEndpoint: oauthSystem.oauthConfig.tokenEndpoint, - authEndpoint: oauthSystem.oauthConfig.authEndpoint, - responseType: oauthSystem.oauthConfig.responseType, - scope: oauthSystem.oauthConfig.scope, - provider: oauthSystem.oauthConfig.provider, - logoutEndpoint: oauthSystem.oauthConfig.logoutEndpoint, - issuer: oauthSystem.oauthConfig.issuer, - jwksEndpoint: oauthSystem.oauthConfig.jwksEndpoint, - }), - }), - ]) - ); - }); - }); - - describe('when identity management is not available', () => { - const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId(); - const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId(); - const setup = () => { - systemRepoMock.findByFilter.mockImplementation((type: SystemTypeEnum) => { - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([oauthSystem]); - if (type === SystemTypeEnum.OIDC) return Promise.resolve([oidcSystem]); - return Promise.resolve([]); - }); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(false); - }; - it('should filter out oidc systems', async () => { - setup(); - const result = await systemService.findByType(SystemTypeEnum.OAUTH); - expect(result).toStrictEqual(SystemMapper.mapFromEntitiesToDtos([oauthSystem])); - }); - }); - }); - - describe('save', () => { - describe('when creating a new system', () => { - const newSystem = systemEntityFactory.build(); - const setup = () => { - systemRepoMock.save.mockResolvedValue(); - }; - - it('should save new system', async () => { - setup(); - const result = await systemService.save(newSystem); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(newSystem)); - }); - }); - - describe('when updating an existing system', () => { - const existingSystem = systemEntityFactory.buildWithId(); - const setup = () => { - systemRepoMock.findById.mockResolvedValue(existingSystem); - }; - - it('should update existing system', async () => { - setup(); - const result = await systemService.save(existingSystem); - expect(systemRepoMock.findById).toHaveBeenCalledTimes(1); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(existingSystem)); - }); - }); - }); -}); diff --git a/apps/server/src/modules/system/service/legacy-system.service.ts b/apps/server/src/modules/system/service/legacy-system.service.ts deleted file mode 100644 index 870219dd92c..00000000000 --- a/apps/server/src/modules/system/service/legacy-system.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { IdentityManagementOauthService } from '@infra/identity-management'; -import { Injectable } from '@nestjs/common'; -import { EntityNotFoundError } from '@shared/common'; -import { SystemEntity } from '@shared/domain/entity'; -import { EntityId, SystemTypeEnum } from '@shared/domain/types'; -import { LegacySystemRepo } from '@shared/repo'; -import { SystemMapper } from '../mapper'; -import { SystemDto } from './dto'; - -// TODO N21-1547: Fully replace this service with SystemService -/** - * @deprecated use {@link SystemService} - */ -@Injectable() -export class LegacySystemService { - constructor( - private readonly systemRepo: LegacySystemRepo, - private readonly idmOauthService: IdentityManagementOauthService - ) {} - - async findById(id: EntityId): Promise { - let system = await this.systemRepo.findById(id); - [system] = await this.generateBrokerSystems([system]); - if (!system) { - throw new EntityNotFoundError(SystemEntity.name, { id }); - } - return SystemMapper.mapFromEntityToDto(system); - } - - async findByType(type?: SystemTypeEnum): Promise { - let systems: SystemEntity[]; - if (type && type === SystemTypeEnum.OAUTH) { - const oauthSystems = await this.systemRepo.findByFilter(SystemTypeEnum.OAUTH); - const oidcSystems = await this.systemRepo.findByFilter(SystemTypeEnum.OIDC); - systems = [...oauthSystems, ...oidcSystems]; - } else if (type) { - systems = await this.systemRepo.findByFilter(type); - } else { - systems = await this.systemRepo.findAll(); - } - systems = await this.generateBrokerSystems(systems); - return SystemMapper.mapFromEntitiesToDtos(systems); - } - - async save(systemDto: SystemDto): Promise { - let system: SystemEntity; - if (systemDto.id) { - system = await this.systemRepo.findById(systemDto.id); - system.type = systemDto.type; - system.alias = systemDto.alias; - system.displayName = systemDto.displayName; - system.oauthConfig = systemDto.oauthConfig; - system.provisioningStrategy = systemDto.provisioningStrategy; - system.provisioningUrl = systemDto.provisioningUrl; - system.url = systemDto.url; - } else { - system = new SystemEntity({ - type: systemDto.type, - alias: systemDto.alias, - displayName: systemDto.displayName, - oauthConfig: systemDto.oauthConfig, - provisioningStrategy: systemDto.provisioningStrategy, - provisioningUrl: systemDto.provisioningUrl, - url: systemDto.url, - }); - } - await this.systemRepo.save(system); - return SystemMapper.mapFromEntityToDto(system); - } - - private async generateBrokerSystems(systems: SystemEntity[]): Promise<[] | SystemEntity[]> { - if (!(await this.idmOauthService.isOauthConfigAvailable())) { - return systems.filter((system) => !(system.oidcConfig && !system.oauthConfig)); - } - const brokerConfig = await this.idmOauthService.getOauthConfig(); - let generatedSystem: SystemEntity; - return systems.map((system) => { - if (system.oidcConfig && !system.oauthConfig) { - generatedSystem = new SystemEntity({ - type: SystemTypeEnum.OAUTH, - alias: system.alias, - displayName: system.displayName ? system.displayName : system.alias, - provisioningStrategy: system.provisioningStrategy, - provisioningUrl: system.provisioningUrl, - url: system.url, - }); - generatedSystem.id = system.id; - generatedSystem.oauthConfig = { ...brokerConfig }; - generatedSystem.oauthConfig.idpHint = system.oidcConfig.idpHint; - generatedSystem.oauthConfig.redirectUri += system.id; - return generatedSystem; - } - return system; - }); - } -} diff --git a/apps/server/src/modules/system/service/system-oidc.service.spec.ts b/apps/server/src/modules/system/service/system-oidc.service.spec.ts deleted file mode 100644 index 6c20f4c7958..00000000000 --- a/apps/server/src/modules/system/service/system-oidc.service.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { EntityNotFoundError } from '@shared/common'; -import { SystemEntity } from '@shared/domain/entity'; -import { LegacySystemRepo } from '@shared/repo'; -import { systemEntityFactory } from '@shared/testing'; -import { SystemOidcMapper } from '../mapper/system-oidc.mapper'; -import { SystemOidcService } from './system-oidc.service'; - -describe('SystemService', () => { - let module: TestingModule; - let systemService: SystemOidcService; - let systemRepoMock: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - SystemOidcService, - { - provide: LegacySystemRepo, - useValue: createMock(), - }, - ], - }).compile(); - systemRepoMock = module.get(LegacySystemRepo); - systemService = module.get(SystemOidcService); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('findById', () => { - const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); - const standaloneSystem = systemEntityFactory.buildWithId({ alias: 'standaloneSystem' }); - const setup = (system: SystemEntity) => { - systemRepoMock.findById.mockResolvedValue(system); - }; - - it('should return a found oidc system', async () => { - setup(oidcSystem); - const result = await systemService.findById('someMockedId'); - expect(result).toStrictEqual(SystemOidcMapper.mapFromEntityToDto(oidcSystem)); - }); - - it('should throw if system does not contain a oidc config', async () => { - setup(standaloneSystem); - await expect(systemService.findById('someMockedId')).rejects.toThrow(EntityNotFoundError); - }); - }); - - describe('findAll', () => { - const ldapSystem = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'ldapSystem' }); - const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); - const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); - - it('should return oidc systems only', async () => { - systemRepoMock.findByFilter.mockResolvedValue([ldapSystem, oauthSystem, oidcSystem]); - const result = await systemService.findAll(); - expect(result).toEqual(expect.arrayContaining(SystemOidcMapper.mapFromEntitiesToDtos([oidcSystem]))); - }); - - it('should return empty list if no oidc system exists', async () => { - systemRepoMock.findByFilter.mockResolvedValue([ldapSystem, oauthSystem]); - const result = await systemService.findAll(); - expect(result).toHaveLength(0); - }); - }); -}); diff --git a/apps/server/src/modules/system/service/system-oidc.service.ts b/apps/server/src/modules/system/service/system-oidc.service.ts deleted file mode 100644 index 6a0e7651bab..00000000000 --- a/apps/server/src/modules/system/service/system-oidc.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityNotFoundError } from '@shared/common'; -import { SystemEntity } from '@shared/domain/entity'; -import { EntityId, SystemTypeEnum } from '@shared/domain/types'; -import { LegacySystemRepo } from '@shared/repo'; -import { SystemOidcMapper } from '../mapper'; -import { OidcConfigDto } from './dto'; - -@Injectable() -export class SystemOidcService { - constructor(private readonly systemRepo: LegacySystemRepo) {} - - async findById(id: EntityId): Promise { - const system = await this.systemRepo.findById(id); - const mappedEntity = SystemOidcMapper.mapFromEntityToDto(system); - if (!mappedEntity) { - throw new EntityNotFoundError(SystemEntity.name, { id }); - } - return mappedEntity; - } - - async findAll(): Promise<[] | OidcConfigDto[]> { - const system = await this.systemRepo.findByFilter(SystemTypeEnum.OIDC); - return SystemOidcMapper.mapFromEntitiesToDtos(system); - } -} 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 5c1eec8f486..5c835580837 100644 --- a/apps/server/src/modules/system/service/system.service.spec.ts +++ b/apps/server/src/modules/system/service/system.service.spec.ts @@ -1,8 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { systemFactory } from '@shared/testing'; -import { SYSTEM_REPO, SystemRepo } from '../domain'; +import { SYSTEM_REPO, SystemQuery, SystemRepo, SystemType } from '../domain'; import { SystemService } from './system.service'; describe(SystemService.name, () => { @@ -34,6 +35,36 @@ describe(SystemService.name, () => { jest.resetAllMocks(); }); + describe('find', () => { + describe('when searching systems with filter', () => { + const setup = () => { + const systems = systemFactory.buildList(1, { type: SystemType.OAUTH }); + + systemRepo.find.mockResolvedValueOnce(systems); + + return { + systems, + }; + }; + + it('should call the repo', async () => { + setup(); + + await service.find({ types: [SystemType.OAUTH] }); + + expect(systemRepo.find).toHaveBeenCalledWith<[SystemQuery]>({ types: [SystemType.OAUTH] }); + }); + + it('should return the systems', async () => { + const { systems } = setup(); + + const result = await service.find({ types: [SystemType.OAUTH] }); + + expect(result).toEqual(systems); + }); + }); + }); + describe('findById', () => { describe('when the system exists', () => { const setup = () => { @@ -70,6 +101,40 @@ describe(SystemService.name, () => { }); }); + describe('findByIdOrFail', () => { + describe('when the system exists', () => { + const setup = () => { + const system = systemFactory.build(); + + systemRepo.getSystemById.mockResolvedValueOnce(system); + + return { + system, + }; + }; + + it('should return the system', async () => { + const { system } = setup(); + + const result = await service.findByIdOrFail(system.id); + + expect(result).toEqual(system); + }); + }); + + describe('when the system does not exist', () => { + const setup = () => { + systemRepo.getSystemById.mockResolvedValueOnce(null); + }; + + it('should return null', async () => { + setup(); + + await expect(service.findByIdOrFail(new ObjectId().toHexString())).rejects.toThrow(NotFoundLoggableException); + }); + }); + }); + describe('getSystems', () => { describe('when systems exist', () => { const setup = () => { @@ -122,6 +187,36 @@ describe(SystemService.name, () => { }); }); + describe('findAllForLdapLogin', () => { + describe('when searching for login ldap systems', () => { + const setup = () => { + const systems = systemFactory.buildList(1, { type: SystemType.LDAP }); + + systemRepo.findAllForLdapLogin.mockResolvedValueOnce(systems); + + return { + systems, + }; + }; + + it('should call the repo', async () => { + setup(); + + await service.findAllForLdapLogin(); + + expect(systemRepo.findAllForLdapLogin).toHaveBeenCalledWith(); + }); + + it('should return the systems', async () => { + const { systems } = setup(); + + const result = await service.findAllForLdapLogin(); + + expect(result).toEqual(systems); + }); + }); + }); + describe('delete', () => { describe('when the system was deleted', () => { const setup = () => { diff --git a/apps/server/src/modules/system/service/system.service.ts b/apps/server/src/modules/system/service/system.service.ts index 590ad227868..4b83558cac9 100644 --- a/apps/server/src/modules/system/service/system.service.ts +++ b/apps/server/src/modules/system/service/system.service.ts @@ -1,25 +1,42 @@ import { Inject, Injectable } from '@nestjs/common'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId } from '@shared/domain/types'; -import { SYSTEM_REPO, System, SystemRepo } from '../domain'; +import { System, SYSTEM_REPO, SystemQuery, SystemRepo } from '../domain'; @Injectable() export class SystemService { constructor(@Inject(SYSTEM_REPO) private readonly systemRepo: SystemRepo) {} + async find(filter: SystemQuery): Promise { + const systems: System[] = await this.systemRepo.find(filter); + + return systems; + } + public async findById(id: EntityId): Promise { - const system = await this.systemRepo.getSystemById(id); + const system: System | null = await this.systemRepo.getSystemById(id); + + return system; + } + + public async findByIdOrFail(id: EntityId): Promise { + const system: System | null = await this.systemRepo.getSystemById(id); + + if (!system) { + throw new NotFoundLoggableException(System.name, { id }); + } return system; } public async getSystems(id: EntityId[]): Promise { - const systems = await this.systemRepo.getSystemsByIds(id); + const systems: System[] = await this.systemRepo.getSystemsByIds(id); return systems; } public async findAllForLdapLogin(): Promise { - const systems = await this.systemRepo.findAllForLdapLogin(); + const systems: System[] = await this.systemRepo.findAllForLdapLogin(); return systems; } diff --git a/apps/server/src/modules/system/system-api.module.ts b/apps/server/src/modules/system/system-api.module.ts index 54592a5c4e6..a2c12f98733 100644 --- a/apps/server/src/modules/system/system-api.module.ts +++ b/apps/server/src/modules/system/system-api.module.ts @@ -1,12 +1,12 @@ import { AuthorizationModule } from '@modules/authorization'; -import { LegacySchoolModule } from '@modules/legacy-school'; -import { SystemController } from '@modules/system/controller/system.controller'; -import { SystemUc } from '@modules/system/uc/system.uc'; import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { SystemController } from './controller'; import { SystemModule } from './system.module'; +import { SystemUc } from './uc/system.uc'; @Module({ - imports: [SystemModule, AuthorizationModule, LegacySchoolModule], + imports: [CqrsModule, SystemModule, AuthorizationModule], providers: [SystemUc], controllers: [SystemController], }) diff --git a/apps/server/src/modules/system/system.module.ts b/apps/server/src/modules/system/system.module.ts index e2d358fb061..7fa512e528f 100644 --- a/apps/server/src/modules/system/system.module.ts +++ b/apps/server/src/modules/system/system.module.ts @@ -1,20 +1,12 @@ import { IdentityManagementModule } from '@infra/identity-management/identity-management.module'; import { Module } from '@nestjs/common'; -import { LegacySystemRepo } from '@shared/repo'; import { SYSTEM_REPO } from './domain'; -import { SystemMikroOrmRepo } from './repo/mikro-orm/system.repo'; -import { LegacySystemService, SystemService } from './service'; -import { SystemOidcService } from './service/system-oidc.service'; +import { SystemMikroOrmRepo } from './repo'; +import { SystemService } from './service'; @Module({ imports: [IdentityManagementModule], - providers: [ - LegacySystemRepo, - LegacySystemService, - SystemOidcService, - SystemService, - { provide: SYSTEM_REPO, useClass: SystemMikroOrmRepo }, - ], - exports: [LegacySystemService, SystemOidcService, SystemService], + providers: [SystemService, { provide: SYSTEM_REPO, useClass: SystemMikroOrmRepo }], + exports: [SystemService], }) export class SystemModule {} diff --git a/apps/server/src/modules/system/uc/system.uc.spec.ts b/apps/server/src/modules/system/uc/system.uc.spec.ts index 80202175beb..088a219e90e 100644 --- a/apps/server/src/modules/system/uc/system.uc.spec.ts +++ b/apps/server/src/modules/system/uc/system.uc.spec.ts @@ -1,34 +1,22 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; import { SystemUc } from '@modules/system/uc/system.uc'; +import { EventBus } from '@nestjs/cqrs'; import { Test, TestingModule } from '@nestjs/testing'; -import { EntityNotFoundError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { SystemEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { EntityId, SystemTypeEnum } from '@shared/domain/types'; -import { legacySchoolDoFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; -import { SystemType } from '../domain'; -import { SystemMapper } from '../mapper'; -import { LegacySystemService, SystemService } from '../service'; +import { setupEntities, systemFactory, userFactory } from '@shared/testing'; +import { SystemDeletedEvent, SystemQuery, SystemType } from '../domain'; +import { SystemService } from '../service'; describe('SystemUc', () => { let module: TestingModule; let systemUc: SystemUc; - let mockSystem1: SystemDto; - let mockSystem2: SystemDto; - let mockSystems: SystemDto[]; - let system1: SystemEntity; - let system2: SystemEntity; - let legacySystemService: DeepMocked; let systemService: DeepMocked; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let eventBus: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -36,10 +24,6 @@ describe('SystemUc', () => { module = await Test.createTestingModule({ providers: [ SystemUc, - { - provide: LegacySystemService, - useValue: createMock(), - }, { provide: SystemService, useValue: createMock(), @@ -49,17 +33,16 @@ describe('SystemUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: EventBus, + useValue: createMock(), }, ], }).compile(); systemUc = module.get(SystemUc); - legacySystemService = module.get(LegacySystemService); systemService = module.get(SystemService); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + eventBus = module.get(EventBus); }); afterAll(async () => { @@ -70,97 +53,126 @@ describe('SystemUc', () => { jest.clearAllMocks(); }); - describe('findByFilter', () => { - beforeEach(() => { - system1 = systemEntityFactory.buildWithId(); - system2 = systemEntityFactory.buildWithId(); + describe('find', () => { + describe('when no query is provided', () => { + const setup = () => { + const oauthSystem = systemFactory.withOauthConfig().build(); + const ldapSystem = systemFactory.withLdapConfig({ active: true }).build(); + const deactivatedLdapSystem = systemFactory.withLdapConfig({ active: false }).build(); + + systemService.find.mockResolvedValueOnce([oauthSystem, ldapSystem, deactivatedLdapSystem]); + + return { + oauthSystem, + ldapSystem, + deactivatedLdapSystem, + }; + }; + + it('should find all systems', async () => { + setup(); - mockSystem1 = SystemMapper.mapFromEntityToDto(system1); - mockSystem2 = SystemMapper.mapFromEntityToDto(system2); - mockSystems = [mockSystem1, mockSystem2]; + await systemUc.find(); - legacySystemService.findByType.mockImplementation((type: string | undefined) => { - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([mockSystem1]); - return Promise.resolve(mockSystems); + expect(systemService.find).toHaveBeenCalledWith({}); }); - legacySystemService.findById.mockImplementation( - (id: EntityId): Promise => (id === system1.id ? Promise.resolve(mockSystem1) : Promise.reject()) - ); - }); - it('should return systems by default', async () => { - const systems: SystemDto[] = await systemUc.findByFilter(); + it('should return all active systems', async () => { + const { oauthSystem, ldapSystem } = setup(); + + const result = await systemUc.find(); - expect(systems.length).toEqual(mockSystems.length); - expect(systems).toContainEqual(expect.objectContaining({ alias: system1.alias })); - expect(systems).toContainEqual(expect.objectContaining({ alias: system2.alias })); + expect(result).toEqual([oauthSystem, ldapSystem]); + }); }); - it('should return specified systems by type', async () => { - const systems: SystemDto[] = await systemUc.findByFilter(SystemTypeEnum.OAUTH); + describe('when types are provided', () => { + const setup = () => { + const ldapSystem = systemFactory.withLdapConfig({ active: true }).build(); + const deactivatedLdapSystem = systemFactory.withLdapConfig({ active: false }).build(); - expect(systems.length).toEqual(1); - expect(systems[0].oauthConfig?.clientId).toEqual(system1.oauthConfig?.clientId); - }); + systemService.find.mockResolvedValueOnce([ldapSystem, deactivatedLdapSystem]); - it('should return oauth systems if requested', async () => { - const systems: SystemDto[] = await systemUc.findByFilter(undefined, true); + return { + ldapSystem, + deactivatedLdapSystem, + }; + }; - expect(systems.length).toEqual(1); - expect(systems[0].oauthConfig?.clientId).toEqual(system2.oauthConfig?.clientId); - }); + it('should find all systems of this type', async () => { + setup(); - it('should return empty system list, because none exist', async () => { - legacySystemService.findByType.mockResolvedValue([]); - const resultResponse = await systemUc.findByFilter(); - expect(resultResponse).toHaveLength(0); + await systemUc.find([SystemType.LDAP]); + + expect(systemService.find).toHaveBeenCalledWith<[SystemQuery]>({ types: [SystemType.LDAP] }); + }); + + it('should return all active systems of this type', async () => { + const { ldapSystem } = setup(); + + const result = await systemUc.find(); + + expect(result).toEqual([ldapSystem]); + }); }); }); describe('findById', () => { - beforeEach(() => { - system1 = systemEntityFactory.buildWithId(); - system2 = systemEntityFactory.buildWithId(); + describe('when a system with the id exists', () => { + const setup = () => { + const system = systemFactory.withOauthConfig().build(); - mockSystem1 = SystemMapper.mapFromEntityToDto(system1); - mockSystem2 = SystemMapper.mapFromEntityToDto(system2); - mockSystems = [mockSystem1, mockSystem2]; + systemService.findById.mockResolvedValueOnce(system); - legacySystemService.findByType.mockImplementation((type: string | undefined) => { - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([mockSystem1]); - return Promise.resolve(mockSystems); + return { + system, + }; + }; + + it('should find the system by id', async () => { + const { system } = setup(); + + await systemUc.findById(system.id); + + expect(systemService.findById).toHaveBeenCalledWith(system.id); }); - legacySystemService.findById.mockImplementation( - (id: EntityId): Promise => (id === system1.id ? Promise.resolve(mockSystem1) : Promise.reject()) - ); - }); - it('should return a system by id', async () => { - const receivedSystem: SystemDto = await systemUc.findById(system1.id); + it('should return the system', async () => { + const { system } = setup(); - expect(receivedSystem.alias).toEqual(system1.alias); - }); + const result = await systemUc.findById(system.id); - it('should reject promise, because no entity was found', async () => { - await expect(systemUc.findById('unknown id')).rejects.toEqual(undefined); + expect(result).toEqual(system); + }); }); - describe('when the ldap is not active', () => { + describe('when no system with the id exists', () => { const setup = () => { - const system: SystemDto = new SystemDto({ - ldapActive: false, - type: 'ldap', - }); - - legacySystemService.findById.mockResolvedValue(system); + systemService.findById.mockResolvedValueOnce(null); }; - it('should reject promise, because ldap is not active', async () => { + it('should throw an error', async () => { setup(); - const func = async () => systemUc.findById('id'); + await expect(systemUc.findById(new ObjectId().toHexString())).rejects.toThrow(NotFoundLoggableException); + }); + }); + + describe('when the system is a deactivated ldap system', () => { + const setup = () => { + const system = systemFactory.withLdapConfig({ active: false }).build(); - await expect(func).rejects.toThrow(EntityNotFoundError); + systemService.findById.mockResolvedValueOnce(system); + + return { + system, + }; + }; + + it('should throw an error', async () => { + const { system } = setup(); + + await expect(systemUc.findById(system.id)).rejects.toThrow(NotFoundLoggableException); }); }); }); @@ -170,22 +182,13 @@ describe('SystemUc', () => { const setup = () => { const user = userFactory.buildWithId(); const system = systemFactory.build(); - const otherSystemId = new ObjectId().toHexString(); - const school = legacySchoolDoFactory.build({ - systems: [system.id, otherSystemId], - ldapLastSync: new Date().toString(), - externalId: 'test', - }); systemService.findById.mockResolvedValueOnce(system); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); return { user, system, - school, - otherSystemId, }; }; @@ -210,50 +213,11 @@ describe('SystemUc', () => { }); it('should remove the system from the school', async () => { - const { user, system, school, otherSystemId } = setup(); - - await systemUc.delete(user.id, user.school.id, system.id); - - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - systems: [otherSystemId], - ldapLastSync: undefined, - externalId: school.externalId, - }) - ); - }); - }); - - describe('when the system is the last ldap system at the school', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const system = systemFactory.build({ type: SystemType.LDAP }); - const school = legacySchoolDoFactory.build({ - systems: [system.id], - ldapLastSync: new Date().toString(), - externalId: 'test', - }); - - systemService.findById.mockResolvedValueOnce(system); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - - return { - user, - system, - }; - }; - - it('should remove the external id of the school', async () => { const { user, system } = setup(); await systemUc.delete(user.id, user.school.id, system.id); - expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ - externalId: undefined, - }) - ); + expect(eventBus.publish).toHaveBeenCalledWith(new SystemDeletedEvent({ schoolId: user.school.id, system })); }); }); diff --git a/apps/server/src/modules/system/uc/system.uc.ts b/apps/server/src/modules/system/uc/system.uc.ts index 9a13ad77214..01e69469b28 100644 --- a/apps/server/src/modules/system/uc/system.uc.ts +++ b/apps/server/src/modules/system/uc/system.uc.ts @@ -1,43 +1,34 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; import { Injectable } from '@nestjs/common'; -import { EntityNotFoundError } from '@shared/common'; +import { EventBus } from '@nestjs/cqrs'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { SystemEntity, User } from '@shared/domain/entity'; +import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { EntityId, SystemTypeEnum } from '@shared/domain/types'; -import { System, SystemType } from '../domain'; -import { LegacySystemService, SystemDto, SystemService } from '../service'; +import { EntityId } from '@shared/domain/types'; +import { System, SystemDeletedEvent, SystemType } from '../domain'; +import { SystemService } from '../service'; @Injectable() export class SystemUc { constructor( - private readonly legacySystemService: LegacySystemService, private readonly systemService: SystemService, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService + private readonly eventBus: EventBus ) {} - async findByFilter(type?: SystemTypeEnum, onlyOauth = false): Promise { - let systems: SystemDto[]; + async find(types?: SystemType[]): Promise { + let systems: System[] = await this.systemService.find({ types }); - if (onlyOauth) { - systems = await this.legacySystemService.findByType(SystemTypeEnum.OAUTH); - } else { - systems = await this.legacySystemService.findByType(type); - } - - systems = systems.filter((system: SystemDto) => system.ldapActive !== false); + systems = systems.filter((system: System) => system.ldapConfig?.active !== false); return systems; } - async findById(id: EntityId): Promise { - const system: SystemDto = await this.legacySystemService.findById(id); + async findById(systemId: EntityId): Promise { + const system: System | null = await this.systemService.findById(systemId); - if (system.ldapActive === false) { - throw new EntityNotFoundError(SystemEntity.name, { id }); + if (!system || system.ldapConfig?.active === false) { + throw new NotFoundLoggableException(System.name, { id: systemId }); } return system; @@ -59,19 +50,6 @@ export class SystemUc { await this.systemService.delete(system); - await this.removeSystemFromSchool(schoolId, system); - } - - private async removeSystemFromSchool(schoolId: string, system: System) { - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); - - school.systems = school.systems?.filter((schoolSystemId: string) => schoolSystemId !== system.id); - school.ldapLastSync = undefined; - - if (system.type === SystemType.LDAP && school.systems?.length === 0) { - school.externalId = undefined; - } - - await this.schoolService.save(school); + await this.eventBus.publish(new SystemDeletedEvent({ schoolId, system })); } } diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index e3f091139c7..65cb8f82969 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -1,5 +1,6 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; +import { SystemEntity } from '@modules/system/entity'; import { FilterImportUserParams, FilterMatchType, @@ -19,7 +20,7 @@ import { import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { PaginationParams } from '@shared/controller'; -import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { SchoolFeature } from '@shared/domain/types'; import { diff --git a/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts b/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts index a17b555bc3a..4f4f482b8e8 100644 --- a/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts +++ b/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts @@ -1,13 +1,17 @@ import { SchulconnexGroupType, SchulconnexGruppenResponse, SchulconnexResponse } from '@infra/schulconnex-client'; +import { EntityManager } from '@mikro-orm/mongodb'; import { SchulconnexResponseMapper } from '@modules/provisioning'; -import { ImportUser, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { System } from '@modules/system'; +import { SystemEntity } from '@modules/system/entity'; +import { ImportUser, SchoolEntity } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; export class SchulconnexImportUserMapper { public static mapDataToUserImportEntities( response: SchulconnexResponse[], - system: SystemEntity, - school: SchoolEntity + system: System, + school: SchoolEntity, + em: EntityManager ): ImportUser[] { const importUsers: ImportUser[] = response.map((externalUser: SchulconnexResponse): ImportUser => { const role: RoleName = SchulconnexResponseMapper.mapSanisRoleToRoleName(externalUser); @@ -16,7 +20,7 @@ export class SchulconnexImportUserMapper { ); const importUser: ImportUser = new ImportUser({ - system, + system: em.getReference(SystemEntity, system.id), school, ldapDn: `uid=${externalUser.person.name.vorname}.${externalUser.person.name.familienname}.${externalUser.pid},`, externalId: externalUser.pid, diff --git a/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts b/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts index 839783e63c8..2c9d6e8d848 100644 --- a/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts +++ b/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts @@ -1,15 +1,19 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { SchulconnexResponse, schulconnexResponseFactory, SchulconnexRestClient } from '@infra/schulconnex-client'; +import type { System } from '@modules/system'; +import { SystemEntity } from '@modules/system/entity'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; -import { ImportUser, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { ImportUser, SchoolEntity } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; import { importUserFactory, schoolEntityFactory, setupEntities, systemEntityFactory, + systemFactory, userDoFactory, } from '@shared/testing'; import { UserImportSchoolExternalIdMissingLoggableException } from '../loggable'; @@ -26,6 +30,7 @@ describe(SchulconnexFetchImportUsersService.name, () => { await setupEntities(); module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], providers: [ SchulconnexFetchImportUsersService, { @@ -73,12 +78,13 @@ describe(SchulconnexFetchImportUsersService.name, () => { describe('when fetching the data', () => { const setup = () => { const externalUserData: SchulconnexResponse = schulconnexResponseFactory.build(); - const system: SystemEntity = systemEntityFactory.buildWithId(); + const system: System = systemFactory.build(); + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); const school: SchoolEntity = schoolEntityFactory.buildWithId({ - systems: [system], + systems: [systemEntity], externalId: 'externalSchoolId', }); - const importUser: ImportUser = createImportUser(externalUserData, school, system); + const importUser: ImportUser = createImportUser(externalUserData, school, systemEntity); schulconnexRestClient.getPersonenInfo.mockResolvedValueOnce([externalUserData]); @@ -111,9 +117,8 @@ describe(SchulconnexFetchImportUsersService.name, () => { describe('when the school has no external id', () => { const setup = () => { - const system: SystemEntity = systemEntityFactory.buildWithId(); + const system: System = systemFactory.build(); const school: SchoolEntity = schoolEntityFactory.buildWithId({ - systems: [system], externalId: undefined, }); diff --git a/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.ts b/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.ts index c5a3f7815eb..2a49438a7c3 100644 --- a/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.ts +++ b/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.ts @@ -1,8 +1,10 @@ import { SchulconnexResponse, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { System } from '@modules/system'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; -import { ImportUser, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { ImportUser, SchoolEntity } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { UserImportSchoolExternalIdMissingLoggableException } from '../loggable'; import { SchulconnexImportUserMapper } from '../mapper'; @@ -11,10 +13,11 @@ import { SchulconnexImportUserMapper } from '../mapper'; export class SchulconnexFetchImportUsersService { constructor( private readonly schulconnexRestClient: SchulconnexRestClient, - private readonly userService: UserService + private readonly userService: UserService, + private readonly em: EntityManager ) {} - public async getData(school: SchoolEntity, system: SystemEntity): Promise { + public async getData(school: SchoolEntity, system: System): Promise { const externalSchoolId: string | undefined = school.externalId; if (!externalSchoolId) { throw new UserImportSchoolExternalIdMissingLoggableException(school.id); @@ -28,7 +31,8 @@ export class SchulconnexFetchImportUsersService { const mappedImportUsers: ImportUser[] = SchulconnexImportUserMapper.mapDataToUserImportEntities( response, system, - school + school, + this.em ); return mappedImportUsers; diff --git a/apps/server/src/modules/user-import/service/user-import.service.spec.ts b/apps/server/src/modules/user-import/service/user-import.service.spec.ts index adfbba48f6d..5575c423610 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.spec.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.spec.ts @@ -2,20 +2,21 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; +import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { SchoolFeature } from '@shared/domain/types'; -import { ImportUserRepo, LegacySystemRepo } from '@shared/repo'; +import { ImportUserRepo } from '@shared/repo'; import { cleanupCollections, importUserFactory, legacySchoolDoFactory, schoolEntityFactory, setupEntities, - systemEntityFactory, + systemFactory, userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; @@ -29,7 +30,7 @@ describe(UserImportService.name, () => { let em: EntityManager; let importUserRepo: DeepMocked; - let legacySystemRepo: DeepMocked; + let systemService: DeepMocked; let userService: DeepMocked; let logger: DeepMocked; let schoolService: DeepMocked; @@ -52,8 +53,8 @@ describe(UserImportService.name, () => { useValue: createMock(), }, { - provide: LegacySystemRepo, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: UserService, @@ -77,7 +78,7 @@ describe(UserImportService.name, () => { service = module.get(UserImportService); em = module.get(EntityManager); importUserRepo = module.get(ImportUserRepo); - legacySystemRepo = module.get(LegacySystemRepo); + systemService = module.get(SystemService); userService = module.get(UserService); logger = module.get(Logger); schoolService = module.get(LegacySchoolService); @@ -115,9 +116,9 @@ describe(UserImportService.name, () => { describe('getMigrationSystem', () => { describe('when fetching the migration system', () => { const setup = () => { - const system: SystemEntity = systemEntityFactory.buildWithId(undefined, features.userMigrationSystemId); + const system: System = systemFactory.build(); - legacySystemRepo.findById.mockResolvedValueOnce(system); + systemService.findByIdOrFail.mockResolvedValueOnce(system); return { system, @@ -127,7 +128,7 @@ describe(UserImportService.name, () => { it('should return the system', async () => { const { system } = setup(); - const result: SystemEntity = await service.getMigrationSystem(); + const result: System = await service.getMigrationSystem(); expect(result).toEqual(system); }); diff --git a/apps/server/src/modules/user-import/service/user-import.service.ts b/apps/server/src/modules/user-import/service/user-import.service.ts index 2f6d8ef6c49..4b47451f74c 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.ts @@ -1,10 +1,11 @@ import { LegacySchoolService } from '@modules/legacy-school'; +import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { SchoolFeature } from '@shared/domain/types'; -import { ImportUserRepo, LegacySystemRepo } from '@shared/repo'; +import { ImportUserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; @@ -13,7 +14,7 @@ import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../log export class UserImportService { constructor( private readonly userImportRepo: ImportUserRepo, - private readonly systemRepo: LegacySystemRepo, + private readonly systemService: SystemService, private readonly userService: UserService, @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, private readonly logger: Logger, @@ -24,10 +25,10 @@ export class UserImportService { await this.userImportRepo.saveImportUsers(importUsers); } - public async getMigrationSystem(): Promise { + public async getMigrationSystem(): Promise { const systemId: string = this.userImportFeatures.userMigrationSystemId; - const system: SystemEntity = await this.systemRepo.findById(systemId); + const system: System = await this.systemService.findByIdOrFail(systemId); return system; } diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts index e741b83316c..f9ca795e3e3 100644 --- a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts @@ -1,10 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; +import { System } from '@modules/system'; +import { SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; -import { ImportUser, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { importUserFactory, setupEntities, systemEntityFactory, userFactory } from '@shared/testing'; +import { importUserFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; @@ -74,13 +76,14 @@ describe(UserImportFetchUc.name, () => { undefined, userImportFeatures.userMigrationSystemId ); + const systemDo: System = systemFactory.build({ id: system.id }); const user: User = userFactory.buildWithId(); const importUser: ImportUser = importUserFactory.build({ system, }); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - userImportService.getMigrationSystem.mockResolvedValueOnce(system); + userImportService.getMigrationSystem.mockResolvedValueOnce(systemDo); schulconnexFetchImportUsersService.getData.mockResolvedValueOnce([importUser]); schulconnexFetchImportUsersService.filterAlreadyMigratedUser.mockResolvedValueOnce([importUser]); userImportService.matchUsers.mockResolvedValueOnce([importUser]); diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts index d68c6e2cdfa..1e08f64f3b2 100644 --- a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts @@ -1,6 +1,7 @@ import { AuthorizationService } from '@modules/authorization'; +import { System } from '@modules/system'; import { Inject, Injectable } from '@nestjs/common'; -import { ImportUser, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { IUserImportFeatures, UserImportFeatures } from '../config'; @@ -22,7 +23,7 @@ export class UserImportFetchUc { const user: User = await this.authorizationService.getUserWithPermissions(currentUserId); this.authorizationService.checkAllPermissions(user, [Permission.IMPORT_USER_MIGRATE]); - const system: SystemEntity = await this.userImportService.getMigrationSystem(); + const system: System = await this.userImportService.getMigrationSystem(); const fetchedData: ImportUser[] = await this.schulconnexFetchImportUsersService.getData(user.school, system); const filteredFetchedData: ImportUser[] = await this.schulconnexFetchImportUsersService.filterAlreadyMigratedUser( 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 705531a3e90..147dba2887e 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 @@ -3,6 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Account, AccountService } from '@modules/account'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { System, SystemService } from '@modules/system'; +import { SystemEntity } from '@modules/system/entity'; import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; @@ -11,21 +13,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { MatchCreatorScope, SchoolFeature } from '@shared/domain/types'; -import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, UserRepo } from '@shared/repo'; import { federalStateFactory, importUserFactory, legacySchoolDoFactory, schoolEntityFactory, setupEntities, + systemEntityFactory, + systemFactory, userDoFactory, userFactory, userLoginMigrationDOFactory, } from '@shared/testing'; -import { systemEntityFactory } from '@shared/testing/factory/systemEntityFactory'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable } from '../loggable'; @@ -44,7 +47,7 @@ describe('[ImportUserModule]', () => { let accountService: DeepMocked; let importUserRepo: DeepMocked; let schoolService: DeepMocked; - let systemRepo: DeepMocked; + let systemService: DeepMocked; let userRepo: DeepMocked; let userService: DeepMocked; let authorizationService: DeepMocked; @@ -74,8 +77,8 @@ describe('[ImportUserModule]', () => { useValue: createMock(), }, { - provide: LegacySystemRepo, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: UserRepo, @@ -116,7 +119,7 @@ describe('[ImportUserModule]', () => { accountService = module.get(AccountService); importUserRepo = module.get(ImportUserRepo); schoolService = module.get(LegacySchoolService); - systemRepo = module.get(LegacySystemRepo); + systemService = module.get(SystemService); userRepo = module.get(UserRepo); userService = module.get(UserService); authorizationService = module.get(AuthorizationService); @@ -795,6 +798,7 @@ describe('[ImportUserModule]', () => { describe('[startSchoolInUserMigration]', () => { let system: SystemEntity; + let systemDo: System; let school: SchoolEntity; let currentUser: User; let userRepoByIdSpy: jest.SpyInstance; @@ -807,6 +811,7 @@ describe('[ImportUserModule]', () => { beforeEach(() => { system = systemEntityFactory.buildWithId({ ldapConfig: {} }); + systemDo = systemFactory.build({ id: system.id, ldapConfig: {} }); school = schoolEntityFactory.buildWithId(); school.officialSchoolNumber = 'foo'; currentUser = userFactory.buildWithId({ school }); @@ -814,7 +819,7 @@ describe('[ImportUserModule]', () => { permissionServiceSpy = authorizationService.checkAllPermissions.mockReturnValue(); schoolServiceSaveSpy = schoolService.save.mockReturnValueOnce(Promise.resolve(createMockSchoolDo(school))); schoolServiceSpy = schoolService.getSchoolById.mockResolvedValue(createMockSchoolDo(school)); - systemRepoSpy = systemRepo.findById.mockReturnValueOnce(Promise.resolve(system)); + systemRepoSpy = systemService.findById.mockReturnValueOnce(Promise.resolve(systemDo)); userImportFeatures.userMigrationSystemId = system.id; dateSpy = jest.spyOn(global, 'Date').mockReturnValue(currentDate as unknown as string); }); @@ -846,7 +851,7 @@ describe('[ImportUserModule]', () => { schoolServiceSaveSpy = schoolService.save.mockImplementation((schoolDo: LegacySchoolDo) => Promise.resolve(schoolDo) ); - userImportService.getMigrationSystem.mockResolvedValueOnce(system); + userImportService.getMigrationSystem.mockResolvedValueOnce(systemDo); await uc.startSchoolInUserMigration(currentUser.id); diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index a77127c51e4..be63375ad4e 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -1,6 +1,7 @@ import { Account, AccountSave, AccountService } from '@modules/account'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; @@ -8,10 +9,10 @@ import { BadRequestException, ForbiddenException, Inject, Injectable } from '@ne import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; -import { ImportUser, MatchCreator, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, MatchCreator, User } from '@shared/domain/entity'; import { IFindOptions, Permission } from '@shared/domain/interface'; import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; -import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { @@ -43,7 +44,7 @@ export class UserImportUc { private readonly importUserRepo: ImportUserRepo, private readonly authorizationService: AuthorizationService, private readonly schoolService: LegacySchoolService, - private readonly systemRepo: LegacySystemRepo, + private readonly systemService: SystemService, private readonly userRepo: UserRepo, private readonly userService: UserService, private readonly logger: Logger, @@ -268,7 +269,7 @@ export class UserImportUc { school.inMaintenanceSince = new Date(); if (useCentralLdap) { - const migrationSystem: SystemEntity = await this.userImportService.getMigrationSystem(); + const migrationSystem: System = await this.userImportService.getMigrationSystem(); if (school.systems && !school.systems.includes(migrationSystem.id)) { school.systems.push(migrationSystem.id); @@ -407,9 +408,9 @@ export class UserImportUc { for (const systemId of school.systems) { // very unusual to have more than 1 system // eslint-disable-next-line no-await-in-loop - const system: SystemEntity = await this.systemRepo.findById(systemId); + const system: System | null = await this.systemService.findById(systemId); - if (system.ldapConfig) { + if (system?.ldapConfig) { throw new LdapAlreadyPersistedException(); } } diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index 6c305a98353..b73db2aebda 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -3,11 +3,12 @@ import { AccountModule } from '@modules/account'; import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; import { OauthModule } from '@modules/oauth'; +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 { ImportUserRepo, LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, LegacySchoolRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { ImportUserController } from './controller/import-user.controller'; import { SchulconnexFetchImportUsersService, UserImportService } from './service'; @@ -26,6 +27,7 @@ import { UserImportConfigModule } from './user-import-config.module'; OauthModule, SchulconnexClientModule.registerAsync(), UserLoginMigrationModule, + SystemModule, ], controllers: [ImportUserController], providers: [ @@ -33,7 +35,6 @@ import { UserImportConfigModule } from './user-import-config.module'; UserImportFetchUc, ImportUserRepo, LegacySchoolRepo, - LegacySystemRepo, UserRepo, UserImportService, SchulconnexFetchImportUsersService, diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration-rollback.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration-rollback.api.spec.ts index 1e9e30ecc6c..940d596d9fa 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration-rollback.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration-rollback.api.spec.ts @@ -1,9 +1,9 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { ServerTestModule } from '@modules/server'; +import { SystemEntity } from '@modules/system/entity'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { SchoolEntity, User } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { cleanupCollections, @@ -13,6 +13,7 @@ import { UserAndAccountTestFactory, userLoginMigrationFactory, } from '@shared/testing'; +import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { Response } from 'supertest'; describe('UserLoginMigrationRollbackController (API)', () => { 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 622bc521952..cb5be880941 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 @@ -4,10 +4,11 @@ import { SchulconnexLizenzInfoResponse } from '@infra/schulconnex-client/respons import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; import { ServerTestModule } from '@modules/server'; +import { type SystemEntity } from '@modules/system/entity'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { ImportUser, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { ImportUser, SchoolEntity, User } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { 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 ee4a7cff300..cb3ecb70348 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 @@ -2,14 +2,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; -import { LegacySystemService } from '@modules/system'; -import { SystemDto } from '@modules/system/service'; +import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; import { EntityId, SchoolFeature } from '@shared/domain/types'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { legacySchoolDoFactory, systemFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { IdenticalUserLoginMigrationSystemLoggableException, MoinSchuleSystemNotFoundLoggableException, @@ -24,7 +23,7 @@ describe(UserLoginMigrationService.name, () => { let userService: DeepMocked; let schoolService: DeepMocked; - let systemService: DeepMocked; + let systemService: DeepMocked; let userLoginMigrationRepo: DeepMocked; const mockedDate: Date = new Date('2023-05-02'); @@ -48,8 +47,8 @@ describe(UserLoginMigrationService.name, () => { useValue: createMock(), }, { - provide: LegacySystemService, - useValue: createMock(), + provide: SystemService, + useValue: createMock(), }, { provide: UserLoginMigrationRepo, @@ -61,7 +60,7 @@ describe(UserLoginMigrationService.name, () => { service = module.get(UserLoginMigrationService); userService = module.get(UserService); schoolService = module.get(LegacySchoolService); - systemService = module.get(LegacySystemService); + systemService = module.get(SystemService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); }); @@ -165,9 +164,8 @@ describe(UserLoginMigrationService.name, () => { const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ + const system: System = systemFactory.withOauthConfig().build({ id: targetSystemId, - type: 'oauth2', alias: 'SANIS', }); @@ -180,7 +178,7 @@ describe(UserLoginMigrationService.name, () => { }); schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); + systemService.find.mockResolvedValue([system]); userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); return { @@ -218,9 +216,8 @@ describe(UserLoginMigrationService.name, () => { const setup = () => { const sourceSystemId: EntityId = new ObjectId().toHexString(); const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ + const system: System = systemFactory.withOauthConfig().build({ id: targetSystemId, - type: 'oauth2', alias: 'SANIS', }); @@ -228,7 +225,7 @@ describe(UserLoginMigrationService.name, () => { const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [sourceSystemId] }, schoolId); schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); + systemService.find.mockResolvedValue([system]); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { @@ -261,14 +258,13 @@ describe(UserLoginMigrationService.name, () => { const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ + const system: System = systemFactory.withOauthConfig().build({ id: targetSystemId, - type: 'oauth2', alias: 'SANIS', }); schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); + systemService.find.mockResolvedValue([system]); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { @@ -298,14 +294,13 @@ describe(UserLoginMigrationService.name, () => { const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ + const system: System = systemFactory.withOauthConfig().build({ id: targetSystemId, - type: 'oauth2', alias: 'SANIS', }); schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); + systemService.find.mockResolvedValue([system]); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { @@ -332,7 +327,7 @@ describe(UserLoginMigrationService.name, () => { const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([]); + systemService.find.mockResolvedValue([]); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { @@ -352,9 +347,8 @@ describe(UserLoginMigrationService.name, () => { describe('when creating a new migration but the SANIS system and schools login system are the same', () => { const setup = () => { const targetSystemId: EntityId = new ObjectId().toHexString(); - const system: SystemDto = new SystemDto({ + const system: System = systemFactory.withOauthConfig().build({ id: targetSystemId, - type: 'oauth2', alias: 'SANIS', }); @@ -362,7 +356,7 @@ describe(UserLoginMigrationService.name, () => { const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [targetSystemId] }, schoolId); schoolService.getSchoolById.mockResolvedValue(school); - systemService.findByType.mockResolvedValue([system]); + systemService.find.mockResolvedValue([system]); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { 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 44876628e7a..2ab7f749934 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,16 +1,17 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { LegacySchoolService } from '@modules/legacy-school'; -import { LegacySystemService, SystemDto } from '@modules/system'; +import { System, SystemService } from '@modules/system'; +import { SystemType } from '@modules/system/domain'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; -import { EntityId, SchoolFeature, SystemTypeEnum } from '@shared/domain/types'; +import { EntityId, SchoolFeature } from '@shared/domain/types'; import { UserLoginMigrationRepo } from '@shared/repo'; import { - UserLoginMigrationAlreadyClosedLoggableException, - UserLoginMigrationGracePeriodExpiredLoggableException, IdenticalUserLoginMigrationSystemLoggableException, MoinSchuleSystemNotFoundLoggableException, + UserLoginMigrationAlreadyClosedLoggableException, + UserLoginMigrationGracePeriodExpiredLoggableException, } from '../loggable'; @Injectable() @@ -19,7 +20,7 @@ export class UserLoginMigrationService { private readonly userService: UserService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo, private readonly schoolService: LegacySchoolService, - private readonly systemService: LegacySystemService + private readonly systemService: SystemService ) {} public async startMigration(schoolId: string): Promise { @@ -111,18 +112,18 @@ export class UserLoginMigrationService { } private async createNewMigration(school: LegacySchoolDo): Promise { - const oauthSystems: SystemDto[] = await this.systemService.findByType(SystemTypeEnum.OAUTH); - const moinSchuleSystem: SystemDto | undefined = oauthSystems.find((system: SystemDto) => system.alias === 'SANIS'); + const oauthSystems: System[] = await this.systemService.find({ types: [SystemType.OAUTH] }); + const moinSchuleSystem: System | undefined = oauthSystems.find((system: System) => system.alias === 'SANIS'); if (!moinSchuleSystem) { throw new MoinSchuleSystemNotFoundLoggableException(); - } else if (school.systems?.includes(moinSchuleSystem.id as string)) { + } else if (school.systems?.includes(moinSchuleSystem.id)) { throw new IdenticalUserLoginMigrationSystemLoggableException(school.id, moinSchuleSystem.id); } const userLoginMigrationDO: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: school.id as string, - targetSystemId: moinSchuleSystem.id as string, + targetSystemId: moinSchuleSystem.id, sourceSystemId: school.systems?.[0], startedAt: new Date(), }); 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 3fce00aac9d..0da0459e6a0 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 @@ -11,11 +11,12 @@ import { ProvisioningService, ProvisioningSystemDto, } from '@modules/provisioning'; +import { SystemEntity } from '@modules/system/entity'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { LegacySchoolDo, Page, UserLoginMigrationDO } from '@shared/domain/domainobject'; -import { SystemEntity, User } from '@shared/domain/entity'; +import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 7dc369e2f04..f45b68d865f 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -1,20 +1,21 @@ import { AccountEntity } from '@modules/account/domain/entity/account.entity'; import { BoardNodeEntity } from '@modules/board/repo/entity'; import { ClassEntity } from '@modules/class/entity'; +import { DeletionLogEntity } from '@modules/deletion/repo/entity/deletion-log.entity'; +import { DeletionRequestEntity } from '@modules/deletion/repo/entity/deletion-request.entity'; import { GroupEntity } from '@modules/group/entity'; import { InstanceEntity } from '@modules/instance'; import { SchoolSystemOptionsEntity } from '@modules/legacy-school/entity'; import { ExternalToolPseudonymEntity, PseudonymEntity } from '@modules/pseudonym/entity'; import { RegistrationPinEntity } from '@modules/registration-pin/entity'; +import { RocketChatUserEntity } from '@modules/rocketchat-user/entity'; import { ShareToken } from '@modules/sharing/entity/share-token.entity'; +import { SystemEntity } from '@modules/system/entity/system.entity'; import { TldrawDrawing } from '@modules/tldraw/entities'; import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { MediaSourceEntity, MediaUserLicenseEntity, UserLicenseEntity } from '@modules/user-license/entity'; -import { DeletionLogEntity } from '@modules/deletion/repo/entity/deletion-log.entity'; -import { DeletionRequestEntity } from '@modules/deletion/repo/entity/deletion-request.entity'; -import { RocketChatUserEntity } from '@modules/rocketchat-user/entity'; import { ColumnBoardNode } from './column-board-node.entity'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -37,7 +38,6 @@ import { SchoolEntity, SchoolRolePermission, SchoolRoles } from './school.entity import { SchoolYearEntity } from './schoolyear.entity'; import { StorageProviderEntity } from './storageprovider.entity'; import { Submission } from './submission.entity'; -import { SystemEntity } from './system.entity'; import { Task } from './task.entity'; import { TeamEntity, TeamUserEntity } from './team.entity'; import { UserLoginMigrationEntity } from './user-login-migration.entity'; diff --git a/apps/server/src/shared/domain/entity/external-source.embeddable.ts b/apps/server/src/shared/domain/entity/external-source.embeddable.ts index 4c5dfe2ac20..6326308a867 100644 --- a/apps/server/src/shared/domain/entity/external-source.embeddable.ts +++ b/apps/server/src/shared/domain/entity/external-source.embeddable.ts @@ -1,5 +1,5 @@ import { Embeddable, ManyToOne, Property } from '@mikro-orm/core'; -import { SystemEntity } from './system.entity'; +import { SystemEntity } from '@modules/system/entity/system.entity'; export interface ExternalSourceEntityProps { externalId: string; diff --git a/apps/server/src/shared/domain/entity/import-user.entity.ts b/apps/server/src/shared/domain/entity/import-user.entity.ts index 78aa5036cb4..24a268ccb5e 100644 --- a/apps/server/src/shared/domain/entity/import-user.entity.ts +++ b/apps/server/src/shared/domain/entity/import-user.entity.ts @@ -1,8 +1,8 @@ import { Entity, Enum, IdentifiedReference, ManyToOne, Property, Unique, wrap } from '@mikro-orm/core'; +import { SystemEntity } from '@modules/system/entity/system.entity'; import { EntityWithSchool, RoleName } from '../interface'; import { BaseEntityReference, BaseEntityWithTimestamps } from './base.entity'; import { SchoolEntity } from './school.entity'; -import { SystemEntity } from './system.entity'; import type { User } from './user.entity'; export type IImportUserRoleName = RoleName.ADMINISTRATOR | RoleName.TEACHER | RoleName.STUDENT; diff --git a/apps/server/src/shared/domain/entity/index.ts b/apps/server/src/shared/domain/entity/index.ts index 9d023584dba..99349a0649e 100644 --- a/apps/server/src/shared/domain/entity/index.ts +++ b/apps/server/src/shared/domain/entity/index.ts @@ -17,7 +17,6 @@ export * from './school.entity'; export * from './schoolyear.entity'; export * from './storageprovider.entity'; export * from './submission.entity'; -export * from './system.entity'; export * from './task.entity'; export * from './team.entity'; export * from './user-login-migration.entity'; diff --git a/apps/server/src/shared/domain/entity/school.entity.ts b/apps/server/src/shared/domain/entity/school.entity.ts index 0d86a420c9d..f152f420fdb 100644 --- a/apps/server/src/shared/domain/entity/school.entity.ts +++ b/apps/server/src/shared/domain/entity/school.entity.ts @@ -12,6 +12,7 @@ import { Property, } from '@mikro-orm/core'; import { SchoolSystemOptionsEntity } from '@modules/legacy-school/entity'; +import { SystemEntity } from '@modules/system/entity/system.entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { SchoolFeature, SchoolPurpose } from '@shared/domain/types'; import { FileStorageType } from '@src/modules/school/domain/type/file-storage-type.enum'; @@ -19,7 +20,6 @@ import { LanguageType } from '../interface'; import { BaseEntityWithTimestamps } from './base.entity'; import { CountyEmbeddable, FederalStateEntity } from './federal-state.entity'; import { SchoolYearEntity } from './schoolyear.entity'; -import { SystemEntity } from './system.entity'; export interface SchoolProperties { _id?: string; 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 bd4f3cf19ba..1b50b43221c 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 @@ -1,6 +1,6 @@ import { Entity, ManyToOne, OneToOne, Property } from '@mikro-orm/core'; +import { SystemEntity } from '@modules/system/entity/system.entity'; import { SchoolEntity } from '@shared/domain/entity/school.entity'; -import { SystemEntity } from '@shared/domain/entity/system.entity'; import { BaseEntityWithTimestamps } from './base.entity'; export type IUserLoginMigration = Readonly>; diff --git a/apps/server/src/shared/repo/index.ts b/apps/server/src/shared/repo/index.ts index de8af17b788..1c4c7b90f97 100644 --- a/apps/server/src/shared/repo/index.ts +++ b/apps/server/src/shared/repo/index.ts @@ -21,7 +21,6 @@ export * from './school'; export * from './schoolexternaltool'; export * from './scope'; export * from './submission'; -export * from './system'; export * from './task'; export * from './teams'; export * from './user'; 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 6bf0f502a04..f3d0ea619a3 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 @@ -2,6 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityData, EntityManager } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain/domainobject'; @@ -10,7 +11,6 @@ import { SchoolRolePermission, SchoolRoles, SchoolYearEntity, - SystemEntity, UserLoginMigrationEntity, } from '@shared/domain/entity'; import { diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.ts b/apps/server/src/shared/repo/school/legacy-school.repo.ts index e61ec482f99..594f0a82ef8 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.ts +++ b/apps/server/src/shared/repo/school/legacy-school.repo.ts @@ -1,8 +1,9 @@ import { EntityData, EntityName } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { LegacySchoolDo } from '@shared/domain/domainobject'; -import { SchoolEntity, SystemEntity, UserLoginMigrationEntity } from '@shared/domain/entity'; +import { SchoolEntity, UserLoginMigrationEntity } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; import { BaseDORepo } from '../base.do.repo'; diff --git a/apps/server/src/shared/repo/scope.ts b/apps/server/src/shared/repo/scope.ts index b66e7c31309..5dd6678cf89 100644 --- a/apps/server/src/shared/repo/scope.ts +++ b/apps/server/src/shared/repo/scope.ts @@ -1,5 +1,5 @@ import { FilterQuery } from '@mikro-orm/core'; -import { EmptyResultQuery } from './query/empty-result.query'; +import { EmptyResultQuery } from './query'; type EmptyResultQueryType = typeof EmptyResultQuery; @@ -32,8 +32,9 @@ export class Scope { this._queries.push(query); } - allowEmptyQuery(isEmptyQueryAllowed: boolean): Scope { + allowEmptyQuery(isEmptyQueryAllowed: boolean): this { this._allowEmptyQuery = isEmptyQueryAllowed; + return this; } } diff --git a/apps/server/src/shared/repo/system/index.ts b/apps/server/src/shared/repo/system/index.ts deleted file mode 100644 index 2c071b949c9..00000000000 --- a/apps/server/src/shared/repo/system/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './legacy-system.repo'; diff --git a/apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts b/apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts deleted file mode 100644 index b8cc14a7311..00000000000 --- a/apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { NotFoundError } from '@mikro-orm/core'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { Test, TestingModule } from '@nestjs/testing'; -import { SystemEntity } from '@shared/domain/entity'; -import { SystemTypeEnum } from '@shared/domain/types'; -import { LegacySystemRepo } from '@shared/repo'; -import { systemEntityFactory } from '@shared/testing'; - -describe('system repo', () => { - let module: TestingModule; - let repo: LegacySystemRepo; - let em: EntityManager; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [MongoMemoryDatabaseModule.forRoot()], - providers: [LegacySystemRepo], - }).compile(); - repo = module.get(LegacySystemRepo); - em = module.get(EntityManager); - }); - - afterAll(async () => { - await module.close(); - }); - - it('should be defined', () => { - expect(repo).toBeDefined(); - expect(typeof repo.findById).toEqual('function'); - }); - - it('should implement entityName getter', () => { - expect(repo.entityName).toBe(SystemEntity); - }); - - describe('findById', () => { - afterEach(async () => { - await em.nativeDelete(SystemEntity, {}); - }); - - it('should return a System that matched by id', async () => { - const system = systemEntityFactory.build(); - await em.persistAndFlush([system]); - const result = await repo.findById(system.id); - expect(result).toEqual(system); - }); - - it('should throw an error if System by id doesnt exist', async () => { - const idA = new ObjectId().toHexString(); - - await expect(repo.findById(idA)).rejects.toThrow(NotFoundError); - }); - }); - - describe('findAll', () => { - afterEach(async () => { - await em.nativeDelete(SystemEntity, {}); - }); - - it('should return all systems', async () => { - const systems = [systemEntityFactory.build(), systemEntityFactory.build({ oauthConfig: undefined })]; - await em.persistAndFlush(systems); - - const result = await repo.findAll(); - - expect(result.length).toEqual(systems.length); - expect(result).toEqual(systems); - }); - }); - - describe('findByFilter', () => { - const ldapSystems = systemEntityFactory.withLdapConfig().buildListWithId(2); - const oauthSystems = systemEntityFactory.withOauthConfig().buildListWithId(2); - const oidcSystems = systemEntityFactory.withOidcConfig().buildListWithId(2); - - beforeAll(async () => { - await em.persistAndFlush([...ldapSystems, ...oauthSystems, ...oidcSystems]); - }); - - afterAll(async () => { - await em.nativeDelete(SystemEntity, {}); - }); - - describe('when searching for a system type', () => { - it('should return ldap systems', async () => { - const result = await repo.findByFilter(SystemTypeEnum.LDAP); - expect(result).toStrictEqual(ldapSystems); - }); - - it('should return oauth systems', async () => { - const result = await repo.findByFilter(SystemTypeEnum.OAUTH); - expect(result).toStrictEqual(oauthSystems); - }); - - it('should return oidc systems', async () => { - const result = await repo.findByFilter(SystemTypeEnum.OIDC); - expect(result).toStrictEqual(oidcSystems); - }); - }); - - describe('when system type is unknown', () => { - it('should throw', async () => { - await expect(repo.findByFilter('keycloak' as unknown as SystemTypeEnum)).rejects.toThrow( - 'system type keycloak unknown' - ); - }); - }); - }); -}); diff --git a/apps/server/src/shared/repo/system/legacy-system.repo.ts b/apps/server/src/shared/repo/system/legacy-system.repo.ts deleted file mode 100644 index f5fa9324bf2..00000000000 --- a/apps/server/src/shared/repo/system/legacy-system.repo.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SystemEntity } from '@shared/domain/entity'; -import { SystemTypeEnum } from '@shared/domain/types'; -import { BaseRepo } from '@shared/repo/base.repo'; -import { SystemScope } from '@shared/repo/system/system-scope'; - -// TODO N21-1547: Fully replace this service with SystemService -/** - * @deprecated use the {@link SystemRepo} from the system module instead - */ -@Injectable() -export class LegacySystemRepo extends BaseRepo { - get entityName() { - return SystemEntity; - } - - async findByFilter(type: SystemTypeEnum): Promise { - const scope = new SystemScope(); - switch (type) { - case SystemTypeEnum.LDAP: - scope.withLdapConfig(); - break; - case SystemTypeEnum.OAUTH: - scope.withOauthConfig(); - break; - case SystemTypeEnum.OIDC: - scope.withOidcConfig(); - break; - default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`system type ${type} unknown`); - } - return this._em.find(SystemEntity, scope.query); - } - - async findAll(): Promise { - return this._em.find(SystemEntity, {}); - } -} diff --git a/apps/server/src/shared/repo/system/system-scope.ts b/apps/server/src/shared/repo/system/system-scope.ts deleted file mode 100644 index 77819f5afa4..00000000000 --- a/apps/server/src/shared/repo/system/system-scope.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SystemEntity } from '@shared/domain/entity'; -import { Scope } from '../scope'; - -export class SystemScope extends Scope { - withLdapConfig(): SystemScope { - this.addQuery({ ldapConfig: { $ne: null } }); - return this; - } - - withOauthConfig(): SystemScope { - this.addQuery({ oauthConfig: { $ne: null } }); - return this; - } - - withOidcConfig(): SystemScope { - this.addQuery({ oidcConfig: { $ne: null } }); - return this; - } -} 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 532a394f6f0..46add47b8e5 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 @@ -3,12 +3,13 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityData, FindOptions, NotFoundError, QueryOrderMap } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { MultipleUsersFoundLoggableException } from '@modules/oauth/loggable'; +import { SystemEntity } from '@modules/system/entity'; import { UserQuery } from '@modules/user/service/user-query.type'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { Page } from '@shared/domain/domainobject/page'; import { UserDO } from '@shared/domain/domainobject/user.do'; -import { Role, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { Role, SchoolEntity, User } from '@shared/domain/entity'; import { IFindOptions, LanguageType, RoleName, SortOrder } from '@shared/domain/interface'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { 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 51834f98050..d499efc45e8 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 @@ -1,8 +1,9 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { type SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; -import { SystemEntity, User } from '@shared/domain/entity'; +import { User } from '@shared/domain/entity'; import { UserParentsEntityProps } from '@shared/domain/entity/user-parents.entity'; import { SortOrder } from '@shared/domain/interface'; import { 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 a762d3eeab2..993f72aedb4 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 @@ -1,14 +1,15 @@ import { createMock } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/mongodb'; +import { type SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { UserLoginMigrationDO } from '@shared/domain/domainobject'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { userLoginMigrationFactory } from '../../testing/factory/user-login-migration.factory'; +import { userLoginMigrationFactory } from '../../testing'; import { UserLoginMigrationRepo } from './user-login-migration.repo'; describe('UserLoginMigrationRepo', () => { diff --git a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.ts b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.ts index 5a35295ac31..4c8598bdc61 100644 --- a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.ts +++ b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.ts @@ -1,8 +1,9 @@ import { EntityData, EntityName } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; +import { SystemEntity } from '@modules/system/entity'; import { Injectable } from '@nestjs/common'; import { UserLoginMigrationDO } from '@shared/domain/domainobject'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; diff --git a/apps/server/src/shared/testing/factory/domainobject/index.ts b/apps/server/src/shared/testing/factory/domainobject/index.ts index 19de632fba1..806f417ce84 100644 --- a/apps/server/src/shared/testing/factory/domainobject/index.ts +++ b/apps/server/src/shared/testing/factory/domainobject/index.ts @@ -5,5 +5,10 @@ export * from './domain-object.factory'; export * from './user-login-migration-do.factory'; export * from './lti-tool.factory'; export * from './pseudonym.factory'; -export { systemFactory } from './system/system.factory'; +export { + systemFactory, + systemOauthConfigFactory, + systemLdapConfigFactory, + systemOidcConfigFactory, +} from './system/system.factory'; export { schoolSystemOptionsFactory } from './school-system-options/school-system-options.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts b/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts index 35e1c3f438c..761345861c4 100644 --- a/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts @@ -1,10 +1,81 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { System, SystemProps } from '@modules/system/domain'; +import { LdapConfig, OauthConfig, OidcConfig, System, SystemProps, SystemType } from '@modules/system/domain'; +import { DeepPartial, Factory } from 'fishery'; import { DomainObjectFactory } from '../domain-object.factory'; -export const systemFactory = DomainObjectFactory.define(System, () => { +export const systemOauthConfigFactory = Factory.define( + () => + new OauthConfig({ + clientId: '12345', + clientSecret: 'mocksecret', + idpHint: 'mock-oauth-idpHint', + tokenEndpoint: 'https://mock.de/mock/auth/public/mockToken', + grantType: 'authorization_code', + redirectUri: 'https://mockhost:3030/api/v3/sso/oauth/', + scope: 'openid uuid', + responseType: 'code', + authEndpoint: 'https://mock.de/auth', + provider: 'mock_type', + logoutEndpoint: 'https://mock.de/logout', + issuer: 'mock_issuer', + jwksEndpoint: 'https://mock.de/jwks', + }) +); + +export const systemLdapConfigFactory = Factory.define( + () => + new LdapConfig({ + active: true, + url: 'ldaps://test.ldap/', + }) +); + +export const systemOidcConfigFactory = Factory.define( + () => + new OidcConfig({ + clientId: 'mock-client-id', + clientSecret: 'mock-client-secret', + idpHint: 'mock-oidc-idpHint', + defaultScopes: 'openid email userinfo', + authorizationUrl: 'https://mock.tld/auth', + tokenUrl: 'https://mock.tld/token', + userinfoUrl: 'https://mock.tld/userinfo', + logoutUrl: 'https://mock.tld/logout', + }) +); + +class SystemFactory extends DomainObjectFactory { + withOauthConfig(params?: DeepPartial): this { + const oauthConfig: OauthConfig = systemOauthConfigFactory.build(params); + + return this.params({ + type: SystemType.OAUTH, + oauthConfig, + }); + } + + withLdapConfig(params?: DeepPartial): this { + const ldapConfig: LdapConfig = systemLdapConfigFactory.build(params); + + return this.params({ + type: SystemType.LDAP, + ldapConfig, + }); + } + + withOidcConfig(params?: DeepPartial): this { + const oidcConfig: OidcConfig = systemOidcConfigFactory.build(params); + + return this.params({ + type: SystemType.OIDC, + oidcConfig, + }); + } +} + +export const systemFactory = SystemFactory.define(System, () => { return { id: new ObjectId().toHexString(), - type: 'oauth2', + type: SystemType.OAUTH, }; }); diff --git a/apps/server/src/shared/testing/factory/systemEntityFactory.ts b/apps/server/src/shared/testing/factory/systemEntityFactory.ts index aca7c9e05c9..f575ee2168b 100644 --- a/apps/server/src/shared/testing/factory/systemEntityFactory.ts +++ b/apps/server/src/shared/testing/factory/systemEntityFactory.ts @@ -4,7 +4,7 @@ import { OidcConfigEntity, SystemEntity, SystemEntityProps, -} from '@shared/domain/entity'; +} from '@modules/system/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { SystemTypeEnum } from '@shared/domain/types'; import { DeepPartial } from 'fishery'; From d882f262014f4703eb19d10fabcd5aea55012ca5 Mon Sep 17 00:00:00 2001 From: virgilchiriac <17074330+virgilchiriac@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:54:19 +0200 Subject: [PATCH 25/35] BC-7461 - upgrade mongodb server to version 6 (#5082) * BC-7461 - update mongo version in CI * BC-7461 - update mongo version for test setup * removing mongo patch version for github workflow * adjusting used mongo version --------- Co-authored-by: MartinSchuhmacher --- .github/workflows/migrations.yml | 4 ++-- .github/workflows/test.yml | 2 +- apps/server/test/globalSetup.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 525dd04c94f..6ca383f87d0 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -1,6 +1,6 @@ name: Reminder to update seed data after migration # If this workflow fails, it is a hint that you forgot to update the seed data after a migration. -# It is only a hint, because it only checks if you updated the migration collection in the seed data. +# It is only a hint, because it only checks if you updated the migration collection in the seed data. # It is not a check that you updated the whole seed data correctly. # See the documentation for advice: https://documentation.dbildungscloud.dev/docs/schulcloud-server/Migrations#committing-a-migration @@ -11,7 +11,7 @@ on: branches: [ main ] env: - MONGODB_VERSION: 5.0 + MONGODB_VERSION: 6.0 NODE_VERSION: '18' jobs: migration: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d759947fea..170ef80cd1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ permissions: contents: read env: - MONGODB_VERSION: 5.0 + MONGODB_VERSION: 6.0 NODE_VERSION: '18' jobs: feathers_tests_cov: diff --git a/apps/server/test/globalSetup.ts b/apps/server/test/globalSetup.ts index 68b50329bb9..0928ed73cf9 100644 --- a/apps/server/test/globalSetup.ts +++ b/apps/server/test/globalSetup.ts @@ -3,7 +3,7 @@ import { MongoMemoryServer } from 'mongodb-memory-server-global'; export = async function globalSetup() { const instance = await MongoMemoryServer.create({ binary: { - version: '5.0.26', + version: '6.0.16', }, }); const uri = instance.getUri(); From 8f1a3d26270070d728a78a6b4467feb38c3b6b57 Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Thu, 11 Jul 2024 14:46:34 +0200 Subject: [PATCH 26/35] N21-2052 ctl template adjustment (#5094) --- .../controller/api-test/tool-configuration.api.spec.ts | 10 ++++++++-- ...xt-external-tool-configuration-template.response.ts | 4 ++++ ...ol-external-tool-configuration-template.response.ts | 4 ++++ .../external-tool/mapper/tool-configuration.mapper.ts | 4 +++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts index 695e5e80066..20154bf1311 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts @@ -6,11 +6,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Course, LegacyBoard, SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { - TestApiClient, - UserAndAccountTestFactory, boardFactory, courseFactory, schoolEntityFactory, + TestApiClient, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; @@ -197,6 +197,7 @@ describe('ToolConfigurationController (API)', () => { externalToolId: externalTool.id, schoolExternalToolId: schoolExternalTool.id, name: externalTool.name, + baseUrl: externalTool.config.baseUrl, logoUrl: externalTool.logoUrl, parameters: [ { @@ -217,6 +218,7 @@ describe('ToolConfigurationController (API)', () => { { externalToolId: externalToolWithoutContextRestriction.id, name: externalToolWithoutContextRestriction.name, + baseUrl: externalToolWithoutContextRestriction.config.baseUrl, parameters: [], schoolExternalToolId: schoolExternalTool2.id, }, @@ -234,6 +236,7 @@ describe('ToolConfigurationController (API)', () => { { externalToolId: externalToolWithoutContextRestriction.id, name: externalToolWithoutContextRestriction.name, + baseUrl: externalToolWithoutContextRestriction.config.baseUrl, parameters: [], schoolExternalToolId: schoolExternalTool2.id, }, @@ -353,6 +356,7 @@ describe('ToolConfigurationController (API)', () => { { externalToolId: externalTool.id, name: externalTool.name, + baseUrl: externalTool.config.baseUrl, logoUrl: externalTool.logoUrl, parameters: [ { @@ -480,6 +484,7 @@ describe('ToolConfigurationController (API)', () => { expect(response.body).toEqual({ externalToolId: externalTool.id, name: externalTool.name, + baseUrl: externalTool.config.baseUrl, logoUrl: externalTool.logoUrl, parameters: [ { @@ -635,6 +640,7 @@ describe('ToolConfigurationController (API)', () => { externalToolId: externalTool.id, schoolExternalToolId: schoolExternalTool.id, name: externalTool.name, + baseUrl: externalTool.config.baseUrl, logoUrl: externalTool.logoUrl, parameters: [ { diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/context-external-tool-configuration-template.response.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/context-external-tool-configuration-template.response.ts index e16495bbacc..be0a5631269 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/context-external-tool-configuration-template.response.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/context-external-tool-configuration-template.response.ts @@ -12,6 +12,9 @@ export class ContextExternalToolConfigurationTemplateResponse { @ApiProperty() name: string; + @ApiProperty() + baseUrl: string; + @ApiPropertyOptional() logoUrl?: string; @@ -22,6 +25,7 @@ export class ContextExternalToolConfigurationTemplateResponse { this.externalToolId = configuration.externalToolId; this.schoolExternalToolId = configuration.schoolExternalToolId; this.name = configuration.name; + this.baseUrl = configuration.baseUrl; this.logoUrl = configuration.logoUrl; this.parameters = configuration.parameters; } diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/school-external-tool-configuration-template.response.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/school-external-tool-configuration-template.response.ts index 85d4d428e94..50915feaaa7 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/school-external-tool-configuration-template.response.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/school-external-tool-configuration-template.response.ts @@ -9,6 +9,9 @@ export class SchoolExternalToolConfigurationTemplateResponse { @ApiProperty() name: string; + @ApiProperty() + baseUrl: string; + @ApiPropertyOptional() logoUrl?: string; @@ -18,6 +21,7 @@ export class SchoolExternalToolConfigurationTemplateResponse { constructor(configuration: SchoolExternalToolConfigurationTemplateResponse) { this.externalToolId = configuration.externalToolId; this.name = configuration.name; + this.baseUrl = configuration.baseUrl; this.logoUrl = configuration.logoUrl; this.parameters = configuration.parameters; } diff --git a/apps/server/src/modules/tool/external-tool/mapper/tool-configuration.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/tool-configuration.mapper.ts index 4821569e872..37d67c810bc 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/tool-configuration.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/tool-configuration.mapper.ts @@ -15,8 +15,9 @@ export class ToolConfigurationMapper { externalTool: ExternalTool ): SchoolExternalToolConfigurationTemplateResponse { const mapped = new SchoolExternalToolConfigurationTemplateResponse({ - externalToolId: externalTool.id ?? '', + externalToolId: externalTool.id, name: externalTool.name, + baseUrl: externalTool.config.baseUrl, logoUrl: externalTool.logoUrl, parameters: externalTool.parameters ? ExternalToolResponseMapper.mapCustomParameterToResponse(externalTool.parameters) @@ -48,6 +49,7 @@ export class ToolConfigurationMapper { externalToolId: externalTool.id ?? '', schoolExternalToolId: schoolExternalTool.id ?? '', name: externalTool.name, + baseUrl: externalTool.config.baseUrl, logoUrl: externalTool.logoUrl, parameters: externalTool.parameters ? ExternalToolResponseMapper.mapCustomParameterToResponse(externalTool.parameters) From 459956e34b0bbefc1f7b6b8308b9e7b5aeb0b71d Mon Sep 17 00:00:00 2001 From: Patrick Sachmann <20001160+psachmann@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:41:23 +0200 Subject: [PATCH 27/35] EW-965 Fixes for latest Keycloak upgrade (#5102) * EW-965 improving logging * EW-965 adding env variables for internal and external base url * Only check unique email for exact matches. --------- Co-authored-by: Maximilian Kreuzkam Co-authored-by: Simone Radtke <94017602+SimoneRadtke-Cap@users.noreply.github.com> Co-authored-by: Simone Radtke --- .../admin-api-server-configmap.yml.j2 | 3 ++- .../identity-management.module.ts | 3 ++- .../interface/keycloak-settings.interface.ts | 3 ++- .../keycloak-config.ts | 3 ++- .../keycloak-administration.service.spec.ts | 5 ++-- .../keycloak-administration.service.ts | 4 ++-- .../keycloak-configuration.service.spec.ts | 3 ++- .../errors/idm-login-error.loggable.spec.ts | 16 +++++++++++++ .../errors/idm-login-error.loggable.ts | 13 +++++++++++ ...-identity-management-oauth.service.spec.ts | 6 ++--- ...cloak-identity-management-oauth.service.ts | 23 +++++++++++-------- .../domain/services/account-idm.service.ts | 4 ++-- config/default.schema.json | 19 ++++++++++++--- config/development.json | 3 ++- config/test.json | 3 ++- 15 files changed, 83 insertions(+), 28 deletions(-) create mode 100644 apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.spec.ts create mode 100644 apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.ts diff --git a/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 b/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 index 8e0e2e76135..8b06a2a96ca 100644 --- a/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 @@ -17,6 +17,7 @@ data: FEATURE_IDENTITY_MANAGEMENT_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_ENABLED }}" FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED }}" FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED }}" - IDENTITY_MANAGEMENT__URI: "{{ IDENTITY_MANAGEMENT__URI }}" + IDENTITY_MANAGEMENT__INTERNAL_URI: "{{ IDENTITY_MANAGEMENT__INTERNAL_URI }}" + IDENTITY_MANAGEMENT__EXTERNAL_URI: "{{ IDENTITY_MANAGEMENT__EXTERNAL_URI }}" IDENTITY_MANAGEMENT__TENANT: "{{ IDENTITY_MANAGEMENT__TENANT }}" IDENTITY_MANAGEMENT__CLIENTID: "{{ IDENTITY_MANAGEMENT__CLIENTID }}" diff --git a/apps/server/src/infra/identity-management/identity-management.module.ts b/apps/server/src/infra/identity-management/identity-management.module.ts index 0a98f025427..9188e08bbe6 100644 --- a/apps/server/src/infra/identity-management/identity-management.module.ts +++ b/apps/server/src/infra/identity-management/identity-management.module.ts @@ -1,5 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; import { EncryptionModule } from '../encryption'; import { IdentityManagementOauthService } from './identity-management-oauth.service'; import { IdentityManagementService } from './identity-management.service'; @@ -9,7 +10,7 @@ import { KeycloakIdentityManagementOauthService } from './keycloak/service/keycl import { KeycloakIdentityManagementService } from './keycloak/service/keycloak-identity-management.service'; @Module({ - imports: [KeycloakModule, KeycloakAdministrationModule, HttpModule, EncryptionModule], + imports: [KeycloakModule, KeycloakAdministrationModule, HttpModule, EncryptionModule, LoggerModule], providers: [ { provide: IdentityManagementService, useClass: KeycloakIdentityManagementService }, { provide: IdentityManagementOauthService, useClass: KeycloakIdentityManagementOauthService }, diff --git a/apps/server/src/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts b/apps/server/src/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts index 6c0b3205669..d2ab4be195a 100644 --- a/apps/server/src/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts +++ b/apps/server/src/infra/identity-management/keycloak-administration/interface/keycloak-settings.interface.ts @@ -1,7 +1,8 @@ export const KeycloakSettings = Symbol('KeycloakSettings'); export interface IKeycloakSettings { - baseUrl: string; + internalBaseUrl: string; + externalBaseUrl: string; realmName: string; clientId: string; credentials: { diff --git a/apps/server/src/infra/identity-management/keycloak-administration/keycloak-config.ts b/apps/server/src/infra/identity-management/keycloak-administration/keycloak-config.ts index 0565ed0b2be..9bba3fa3cbe 100644 --- a/apps/server/src/infra/identity-management/keycloak-administration/keycloak-config.ts +++ b/apps/server/src/infra/identity-management/keycloak-administration/keycloak-config.ts @@ -4,7 +4,8 @@ import { IKeycloakSettings } from './interface/keycloak-settings.interface'; export default class KeycloakAdministration { static keycloakSettings = (Configuration.get('FEATURE_IDENTITY_MANAGEMENT_ENABLED') as boolean) ? ({ - baseUrl: Configuration.get('IDENTITY_MANAGEMENT__URI') as string, + internalBaseUrl: Configuration.get('IDENTITY_MANAGEMENT__INTERNAL_URI') as string, + externalBaseUrl: Configuration.get('IDENTITY_MANAGEMENT__EXTERNAL_URI') as string, realmName: Configuration.get('IDENTITY_MANAGEMENT__TENANT') as string, clientId: Configuration.get('IDENTITY_MANAGEMENT__CLIENTID') as string, credentials: { diff --git a/apps/server/src/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 index 77e372bdc45..af3f51f2960 100644 --- a/apps/server/src/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 @@ -15,7 +15,8 @@ describe('KeycloakAdministrationService', () => { const getSettings = (): IKeycloakSettings => { return { - baseUrl: 'http://localhost:8080', + internalBaseUrl: 'http://localhost:8080', + externalBaseUrl: 'http://localhost:8080', realmName: 'master', clientId: 'client', credentials: { @@ -110,7 +111,7 @@ describe('KeycloakAdministrationService', () => { describe('getWellKnownUrl', () => { it('should return the well known URL', () => { const wellKnownUrl = service.getWellKnownUrl(); - expect(wellKnownUrl).toContain(settings.baseUrl); + expect(wellKnownUrl).toContain(settings.internalBaseUrl); expect(wellKnownUrl).toContain(settings.realmName); }); }); diff --git a/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts b/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts index a6e1669f869..07cf1b84d7a 100644 --- a/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts +++ b/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts @@ -13,7 +13,7 @@ export class KeycloakAdministrationService { @Inject(KeycloakSettings) private readonly kcSettings: IKeycloakSettings ) { this.kcAdminClient.setConfig({ - baseUrl: kcSettings.baseUrl, + baseUrl: kcSettings.internalBaseUrl, realmName: kcSettings.realmName, }); } @@ -33,7 +33,7 @@ export class KeycloakAdministrationService { } public getWellKnownUrl(): string { - return `${this.kcSettings.baseUrl}/realms/${this.kcSettings.realmName}/.well-known/openid-configuration`; + return `${this.kcSettings.externalBaseUrl}/realms/${this.kcSettings.realmName}/.well-known/openid-configuration`; } public getAdminUser(): string { diff --git a/apps/server/src/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 index 7818eda1655..1e7f10c87e5 100644 --- a/apps/server/src/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 @@ -50,7 +50,8 @@ describe('KeycloakConfigurationService Unit', () => { const getSettings = (): IKeycloakSettings => { return { - baseUrl: 'http://localhost:8080', + internalBaseUrl: 'http://localhost:8080', + externalBaseUrl: 'http://localhost:8080', realmName: 'master', clientId: 'dBildungscloud', credentials: { diff --git a/apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.spec.ts b/apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.spec.ts new file mode 100644 index 00000000000..35a0443f91c --- /dev/null +++ b/apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.spec.ts @@ -0,0 +1,16 @@ +import { IDMLoginError } from './idm-login-error.loggable'; + +describe('IDMLoginError', () => { + describe('getLogMessage', () => { + it('should return log message', () => { + const err = new Error(); + const loggable = new IDMLoginError(err); + + expect(loggable.getLogMessage()).toStrictEqual({ + message: 'Error while trying to login via IDM', + stack: err.stack, + type: 'IDM_LOGIN_ERROR', + }); + }); + }); +}); diff --git a/apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.ts b/apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.ts new file mode 100644 index 00000000000..9c2096cd2e0 --- /dev/null +++ b/apps/server/src/infra/identity-management/keycloak/errors/idm-login-error.loggable.ts @@ -0,0 +1,13 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class IDMLoginError implements Loggable { + constructor(private readonly error: Error) {} + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Error while trying to login via IDM', + stack: this.error.stack, + type: 'IDM_LOGIN_ERROR', + }; + } +} diff --git a/apps/server/src/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 index 72f01583aa2..acd84025ad8 100644 --- a/apps/server/src/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 @@ -2,10 +2,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; import { HttpService } from '@nestjs/axios'; -import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; +import { Logger } from '@src/core/logger'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; import { KeycloakIdentityManagementOauthService } from './keycloak-identity-management-oauth.service'; @@ -32,8 +32,8 @@ describe('KeycloakIdentityManagementService', () => { useValue: createMock(), }, { - provide: ConfigService, - useValue: createMock(), + provide: Logger, + useValue: createMock(), }, { provide: DefaultEncryptionService, diff --git a/apps/server/src/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 index 7ee1ce2eb44..6310e50ab76 100644 --- a/apps/server/src/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 @@ -2,10 +2,12 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { OauthConfig } from '@modules/system/domain'; import { HttpService } from '@nestjs/axios'; import { Inject, Injectable } from '@nestjs/common'; +import { Logger } from '@src/core/logger'; import qs from 'qs'; import { lastValueFrom } from 'rxjs'; import { IdentityManagementOauthService } from '../../identity-management-oauth.service'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; +import { IDMLoginError } from '../errors/idm-login-error.loggable'; @Injectable() export class KeycloakIdentityManagementOauthService extends IdentityManagementOauthService { @@ -14,7 +16,8 @@ export class KeycloakIdentityManagementOauthService extends IdentityManagementOa constructor( private readonly kcAdminService: KeycloakAdministrationService, private readonly httpService: HttpService, - @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService + @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService, + private readonly logger: Logger ) { super(); } @@ -54,15 +57,15 @@ export class KeycloakIdentityManagementOauthService extends IdentityManagementOa } async resourceOwnerPasswordGrant(username: string, password: string): Promise { - const { clientId, clientSecret, tokenEndpoint } = await this.getOauthConfig(); - const data = { - username, - password, - grant_type: 'password', - client_id: clientId, - client_secret: this.oAuthEncryptionService.decrypt(clientSecret), - }; try { + const { clientId, clientSecret, tokenEndpoint } = await this.getOauthConfig(); + const data = { + username, + password, + grant_type: 'password', + client_id: clientId, + client_secret: this.oAuthEncryptionService.decrypt(clientSecret), + }; const response = await lastValueFrom( this.httpService.request<{ access_token: string }>({ method: 'post', @@ -75,6 +78,8 @@ export class KeycloakIdentityManagementOauthService extends IdentityManagementOa ); return response.data.access_token; } catch (err) { + this.logger.warning(new IDMLoginError(err as Error)); + return undefined; } } diff --git a/apps/server/src/modules/account/domain/services/account-idm.service.ts b/apps/server/src/modules/account/domain/services/account-idm.service.ts index c12fb5c7b9b..c20b1ec7b14 100644 --- a/apps/server/src/modules/account/domain/services/account-idm.service.ts +++ b/apps/server/src/modules/account/domain/services/account-idm.service.ts @@ -193,8 +193,8 @@ export class AccountServiceIdm extends AbstractAccountService { } public async isUniqueEmail(email: string): Promise { - const [, count] = await this.identityManager.findAccountsByUsername(email); - const isUniqueEmail = count === 0; + const [accounts] = await this.identityManager.findAccountsByUsername(email, { exact: true }); + const isUniqueEmail = accounts.length === 0; return isUniqueEmail; } diff --git a/config/default.schema.json b/config/default.schema.json index db0a429e965..d53b95a6936 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -434,12 +434,25 @@ "IDENTITY_MANAGEMENT": { "type": "object", "description": "Identity management server properties.", - "required": ["URI", "TENANT", "CLIENTID", "ADMIN_CLIENTID", "ADMIN_USER", "ADMIN_PASSWORD"], + "required": [ + "INTERNAL_URI", + "EXTERNAL_URI", + "TENANT", + "CLIENTID", + "ADMIN_CLIENTID", + "ADMIN_USER", + "ADMIN_PASSWORD" + ], "properties": { - "URI": { + "INTERNAL_URI": { + "type": "string", + "default": null, + "description": "The ErWIn IDM base URI for Kubernetes cluster internal use." + }, + "EXTERNAL_URI": { "type": "string", "default": null, - "description": "The ErWIn IDM base URI." + "description": "The ErWIn IDM base URI for Kubernetes cluster external use." }, "TENANT": { "type": "string", diff --git a/config/development.json b/config/development.json index 006601dd3cf..6fe9ff82185 100644 --- a/config/development.json +++ b/config/development.json @@ -40,7 +40,8 @@ }, "FEATURE_IDENTITY_MANAGEMENT_ENABLED": true, "IDENTITY_MANAGEMENT": { - "URI": "http://localhost:8080", + "INTERNAL_URI": "http://localhost:8080", + "EXTERNAL_URI": "http://localhost:8080", "TENANT": "dBildungscloud", "CLIENTID": "dbc", "ADMIN_CLIENTID": "admin-cli", diff --git a/config/test.json b/config/test.json index d69ba6fc8f7..3789f2633c5 100644 --- a/config/test.json +++ b/config/test.json @@ -33,7 +33,8 @@ }, "FEATURE_IDENTITY_MANAGEMENT_ENABLED": true, "IDENTITY_MANAGEMENT": { - "URI": "http://localhost:8080", + "INTERNAL_URI": "http://localhost:8080", + "EXTERNAL_URI": "http://localhost:8080", "TENANT": "master", "CLIENTID": "dbc", "ADMIN_CLIENTID": "admin-cli", From a82c051a6f504980935a956f35d6c3dd9646e473 Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Mon, 15 Jul 2024 10:19:31 +0200 Subject: [PATCH 28/35] N21-1895 refactors configs (#5083) * N21-1895 refactors tool config * N21-1895 refactors bbb config and videoconference config * N21-1895 refactors user import config * N21-1895 refactors provisioning config --- apps/server/src/modules/board/board.module.ts | 6 +-- .../board-node-copy-general.service.spec.ts | 7 +-- .../board-node-copy-specific.service.spec.ts | 29 +++++++----- .../internal/board-node-copy.service.ts | 9 ++-- .../src/modules/learnroom/learnroom.module.ts | 2 - .../service/course-copy.service.spec.ts | 17 ++++--- .../learnroom/service/course-copy.service.ts | 13 ++--- .../oauth-provider/oauth-provider.module.ts | 11 +---- .../oauth-provider.login-flow.service.spec.ts | 23 +++++---- .../oauth-provider.login-flow.service.ts | 8 ++-- .../src/modules/provisioning/config/index.ts | 1 - .../config/provisioning-config.ts | 17 ------- .../provisioning-config.module.ts | 13 ----- .../provisioning/provisioning.config.ts | 2 + .../provisioning/provisioning.module.ts | 2 - .../oidc/schulconnex.strategy.spec.ts | 42 ++++++++--------- .../strategy/oidc/schulconnex.strategy.ts | 10 ++-- .../strategy/sanis/sanis.strategy.spec.ts | 32 +++++-------- .../strategy/sanis/sanis.strategy.ts | 7 +-- .../sanis/schulconnex-response-mapper.spec.ts | 24 ++++------ .../sanis/schulconnex-response-mapper.ts | 9 ++-- .../modules/server/api/dto/config.response.ts | 12 ++--- .../src/modules/server/server.config.ts | 38 +++++++++++---- .../src/modules/server/server.module.ts | 7 ++- .../context-external-tool.module.ts | 2 - .../external-tool/external-tool.module.ts | 3 +- ...xternal-tool-configuration.service.spec.ts | 7 --- ....ts => external-tool-logo.service.spec.ts} | 22 ++++----- .../service/external-tool-logo.service.ts | 22 +++++---- .../external-tool-validation.service.spec.ts | 19 ++++---- apps/server/src/modules/tool/index.ts | 1 - .../school-external-tool.module.ts | 5 +- .../src/modules/tool/tool-api.module.ts | 2 - .../src/modules/tool/tool-config.module.ts | 13 ----- apps/server/src/modules/tool/tool-config.ts | 29 +++--------- apps/server/src/modules/tool/tool.module.ts | 8 ++-- .../src/modules/user-import/config/index.ts | 1 - .../user-import/config/user-import-config.ts | 17 ------- .../api-test/import-user-populate.api.spec.ts | 22 +++++---- .../api-test/import-user.api.spec.ts | 32 ++++++++----- apps/server/src/modules/user-import/index.ts | 3 +- .../service/user-import.service.spec.ts | 30 +++++++----- .../service/user-import.service.ts | 11 +++-- .../uc/user-import-fetch.uc.spec.ts | 32 ++++++++----- .../user-import/uc/user-import-fetch.uc.ts | 14 ++++-- .../user-import/uc/user-import.uc.spec.ts | 47 ++++++++++--------- .../modules/user-import/uc/user-import.uc.ts | 29 +++++++----- .../user-import/user-import-config.module.ts | 13 ----- .../modules/user-import/user-import-config.ts | 3 ++ .../modules/user-import/user-import.module.ts | 2 - .../video-conference/bbb/bbb-config.ts | 5 ++ .../bbb/bbb-settings.interface.ts | 7 --- .../video-conference/bbb/bbb.service.spec.ts | 31 +++++------- .../video-conference/bbb/bbb.service.ts | 14 +++--- .../src/modules/video-conference/bbb/index.ts | 2 +- .../src/modules/video-conference/index.ts | 3 +- .../video-conference/interface/index.ts | 1 - .../video-conference-settings.interface.ts | 9 ---- .../service/video-conference.service.spec.ts | 28 +++++++---- .../service/video-conference.service.ts | 32 ++++++++----- .../video-conference-config.ts | 19 ++------ .../video-conference.module.ts | 12 +---- 62 files changed, 392 insertions(+), 501 deletions(-) delete mode 100644 apps/server/src/modules/provisioning/config/index.ts delete mode 100644 apps/server/src/modules/provisioning/config/provisioning-config.ts delete mode 100644 apps/server/src/modules/provisioning/provisioning-config.module.ts rename apps/server/src/modules/tool/external-tool/service/{external-tool-logo-service.spec.ts => external-tool-logo.service.spec.ts} (95%) delete mode 100644 apps/server/src/modules/tool/tool-config.module.ts delete mode 100644 apps/server/src/modules/user-import/config/index.ts delete mode 100644 apps/server/src/modules/user-import/config/user-import-config.ts delete mode 100644 apps/server/src/modules/user-import/user-import-config.module.ts create mode 100644 apps/server/src/modules/video-conference/bbb/bbb-config.ts delete mode 100644 apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts delete mode 100644 apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 3e7d5cf3c49..884db4ee05a 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -3,7 +3,6 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { FilesStorageClientModule } from '@modules/files-storage-client'; import { TldrawClientModule } from '@modules/tldraw-client'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; -import { ToolConfigModule } from '@modules/tool/tool-config.module'; import { UserModule } from '@modules/user'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; @@ -21,14 +20,14 @@ import { UserDeletedEventHandlerService, } from './service'; import { + BoardContextService, BoardNodeCopyService, + BoardNodeDeleteHooksService, ColumnBoardCopyService, ColumnBoardLinkService, ColumnBoardReferenceService, ColumnBoardTitleService, ContentElementUpdateService, - BoardNodeDeleteHooksService, - BoardContextService, } from './service/internal'; @Module({ @@ -39,7 +38,6 @@ import { UserModule, ContextExternalToolModule, HttpModule, - ToolConfigModule, TldrawClientModule, CqrsModule, CollaborativeTextEditorModule, diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts index 7402a8c1436..6c9201e4f1d 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts @@ -3,7 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { StorageLocation } from '@modules/files-storage/entity'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; @@ -35,8 +36,8 @@ describe(BoardNodeCopyService.name, () => { providers: [ BoardNodeCopyService, { - provide: ToolFeatures, - useValue: createMock(), + provide: ConfigService, + useValue: createMock>(), }, { provide: ContextExternalToolService, diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts index 99299317481..d4c57e4d0e6 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts @@ -3,7 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { StorageLocation } from '@modules/files-storage/entity'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; @@ -43,13 +44,13 @@ import { BoardNodeCopyService } from './board-node-copy.service'; describe(BoardNodeCopyService.name, () => { let module: TestingModule; let service: BoardNodeCopyService; - const toolFeatures: IToolFeatures = { - ctlToolsTabEnabled: false, - ltiToolsTabEnabled: false, - maxExternalToolLogoSizeInBytes: 0, - backEndUrl: '', - ctlToolsCopyEnabled: false, - ctlToolsReloadTimeMs: 0, + const config: ToolConfig = { + FEATURE_CTL_TOOLS_TAB_ENABLED: false, + FEATURE_LTI_TOOLS_TAB_ENABLED: false, + CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES: 0, + CTL_TOOLS_BACKEND_URL: '', + FEATURE_CTL_TOOLS_COPY_ENABLED: false, + CTL_TOOLS_RELOAD_TIME_MS: 0, }; let contextExternalToolService: DeepMocked; let copyHelperService: DeepMocked; @@ -59,8 +60,10 @@ describe(BoardNodeCopyService.name, () => { providers: [ BoardNodeCopyService, { - provide: ToolFeatures, - useValue: toolFeatures, + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof ToolConfig) => config[key]), + }, }, { provide: ContextExternalToolService, @@ -81,7 +84,7 @@ describe(BoardNodeCopyService.name, () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); afterAll(async () => { @@ -404,7 +407,7 @@ describe(BoardNodeCopyService.name, () => { const setupCopyEnabled = () => { const { copyContext, externalToolElement } = setup(); - toolFeatures.ctlToolsCopyEnabled = true; + config.FEATURE_CTL_TOOLS_COPY_ENABLED = true; return { copyContext, externalToolElement }; }; @@ -474,7 +477,7 @@ describe(BoardNodeCopyService.name, () => { const setupCopyDisabled = () => { const { copyContext, externalToolElement } = setup(); - toolFeatures.ctlToolsCopyEnabled = false; + config.FEATURE_CTL_TOOLS_COPY_ENABLED = false; return { copyContext, externalToolElement }; }; diff --git a/apps/server/src/modules/board/service/internal/board-node-copy.service.ts b/apps/server/src/modules/board/service/internal/board-node-copy.service.ts index dd254390eb9..4686f6cfcae 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy.service.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy.service.ts @@ -3,8 +3,9 @@ import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from ' import { CopyFileDto } from '@modules/files-storage-client/dto'; import { ContextExternalToolService } from '@modules/tool/context-external-tool'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; -import { Inject, Injectable } from '@nestjs/common'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain/types'; import { AnyBoardNode, @@ -34,7 +35,7 @@ export interface CopyContext { @Injectable() export class BoardNodeCopyService { constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly configService: ConfigService, private readonly contextExternalToolService: ContextExternalToolService, private readonly copyHelperService: CopyHelperService ) {} @@ -286,7 +287,7 @@ export class BoardNodeCopyService { }); let status: CopyStatusEnum; - if (this.toolFeatures.ctlToolsCopyEnabled && original.contextExternalToolId) { + if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED') && original.contextExternalToolId) { const linkedTool = await this.contextExternalToolService.findById(original.contextExternalToolId); if (linkedTool) { diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 5f112d07dff..4ed7eae9fef 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -3,7 +3,6 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; -import { ToolConfigModule } from '@modules/tool/tool-config.module'; import { forwardRef, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { @@ -44,7 +43,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; LessonModule, LoggerModule, TaskModule, - ToolConfigModule, CqrsModule, ], providers: [ diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts index 91f85457879..8d5242de5e2 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts @@ -4,7 +4,8 @@ import { LessonCopyService } from '@modules/lesson/service'; import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { Course } from '@shared/domain/entity'; import { CourseRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; @@ -32,7 +33,7 @@ describe('course copy service', () => { let copyHelperService: DeepMocked; let userRepo: DeepMocked; let contextExternalToolService: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; afterAll(async () => { await module.close(); @@ -80,10 +81,8 @@ describe('course copy service', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - ctlToolsTabEnabled: false, - }, + provide: ConfigService, + useValue: createMock>(), }, ], }).compile(); @@ -97,7 +96,7 @@ describe('course copy service', () => { copyHelperService = module.get(CopyHelperService); userRepo = module.get(UserRepo); contextExternalToolService = module.get(ContextExternalToolService); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); }); beforeEach(() => { @@ -136,7 +135,7 @@ describe('course copy service', () => { lessonCopyService.updateCopiedEmbeddedTasks.mockReturnValue(boardCopyStatus); - toolFeatures.ctlToolsCopyEnabled = true; + configService.get.mockReturnValue(true); return { user, @@ -383,7 +382,7 @@ describe('course copy service', () => { lessonCopyService.updateCopiedEmbeddedTasks.mockReturnValue(boardCopyStatus); - toolFeatures.ctlToolsCopyEnabled = false; + configService.get.mockReturnValue(false); return { user, diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.ts b/apps/server/src/modules/learnroom/service/course-copy.service.ts index e176ef3ea37..bf425d0c376 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.ts @@ -2,11 +2,12 @@ import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from ' import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { Inject, Injectable } from '@nestjs/common'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Course, User } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; -import { LegacyBoardRepo, CourseRepo, UserRepo } from '@shared/repo'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { CourseRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; import { BoardCopyService } from './board-copy.service'; import { RoomsService } from './rooms.service'; @@ -19,7 +20,7 @@ type CourseCopyParams = { @Injectable() export class CourseCopyService { constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly configService: ConfigService, private readonly courseRepo: CourseRepo, private readonly legacyBoardRepo: LegacyBoardRepo, private readonly roomsService: RoomsService, @@ -52,7 +53,7 @@ export class CourseCopyService { // copy course and board const courseCopy = await this.copyCourseEntity({ user, originalCourse, copyName }); - if (this.toolFeatures.ctlToolsCopyEnabled) { + if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED')) { const contextRef: ContextRef = { id: courseId, type: ToolContextType.COURSE }; const contextExternalToolsInContext: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext(contextRef); @@ -120,7 +121,7 @@ export class CourseCopyService { boardStatus, ]; - if (this.toolFeatures.ctlToolsCopyEnabled) { + if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED')) { elements.push({ type: CopyElementType.EXTERNAL_TOOL, status: CopyStatusEnum.SUCCESS, 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 35a1b9ece82..cc9b95bdabf 100644 --- a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts +++ b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts @@ -2,7 +2,6 @@ import { OauthProviderServiceModule } from '@infra/oauth-provider'; import { LtiToolModule } from '@modules/lti-tool'; import { PseudonymModule } from '@modules/pseudonym'; import { ToolModule } from '@modules/tool'; -import { ToolConfigModule } from '@modules/tool/tool-config.module'; import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { TeamsRepo } from '@shared/repo'; @@ -11,15 +10,7 @@ import { IdTokenService } from './service/id-token.service'; import { OauthProviderLoginFlowService } from './service/oauth-provider.login-flow.service'; @Module({ - imports: [ - OauthProviderServiceModule, - UserModule, - LoggerModule, - PseudonymModule, - LtiToolModule, - ToolModule, - ToolConfigModule, - ], + imports: [OauthProviderServiceModule, UserModule, LoggerModule, PseudonymModule, LtiToolModule, ToolModule], providers: [OauthProviderLoginFlowService, IdTokenService, TeamsRepo], exports: [OauthProviderLoginFlowService, IdTokenService], }) diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts index df118628596..fb308ebe962 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts @@ -3,20 +3,21 @@ import { LtiToolService } from '@modules/lti-tool'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolService } from '@modules/tool/external-tool/service'; import { externalToolFactory } from '@modules/tool/external-tool/testing'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; import { NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiToolDO } from '@shared/domain/domainobject'; import { ltiToolDOFactory, setupEntities } from '@shared/testing'; import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; -describe('OauthProviderLoginFlowService', () => { +describe(OauthProviderLoginFlowService.name, () => { let module: TestingModule; let service: OauthProviderLoginFlowService; let ltiToolService: DeepMocked; let externalToolService: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; beforeAll(async () => { module = await Test.createTestingModule({ @@ -31,10 +32,8 @@ describe('OauthProviderLoginFlowService', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - ctlToolsTabEnabled: false, - }, + provide: ConfigService, + useValue: createMock>(), }, ], }).compile(); @@ -42,7 +41,7 @@ describe('OauthProviderLoginFlowService', () => { service = module.get(OauthProviderLoginFlowService); ltiToolService = module.get(LtiToolService); externalToolService = module.get(ExternalToolService); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); await setupEntities(); }); @@ -58,7 +57,7 @@ describe('OauthProviderLoginFlowService', () => { describe('findToolByClientId', () => { describe('when it finds a ctl tool and the ctl feature is active', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = true; + configService.get.mockReturnValue(true); const tool: ExternalTool = externalToolFactory.buildWithId({ name: 'SchulcloudNextcloud' }); @@ -81,7 +80,7 @@ describe('OauthProviderLoginFlowService', () => { describe('when a lti tool exists and the ctl feature is deactivated', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = false; + configService.get.mockReturnValue(false); const tool: LtiToolDO = ltiToolDOFactory.buildWithId({ name: 'SchulcloudNextcloud' }); @@ -111,7 +110,7 @@ describe('OauthProviderLoginFlowService', () => { describe('when a lti tool exists and the ctl feature is active and no ctl tool exists', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = true; + configService.get.mockReturnValue(true); const tool: LtiToolDO = ltiToolDOFactory.buildWithId({ name: 'SchulcloudNextcloud' }); @@ -142,7 +141,7 @@ describe('OauthProviderLoginFlowService', () => { describe('when no lti or ctl tool was found', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = true; + configService.get.mockReturnValue(true); externalToolService.findExternalToolByOAuth2ConfigClientId.mockResolvedValue(null); ltiToolService.findByClientIdAndIsLocal.mockResolvedValue(null); diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts index adf363415fd..75ed2aaa891 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts @@ -1,10 +1,10 @@ import { LtiToolService } from '@modules/lti-tool/service'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolService } from '@modules/tool/external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; -import { Inject } from '@nestjs/common'; +import { ToolConfig } from '@modules/tool/tool-config'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; +import { ConfigService } from '@nestjs/config'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; @Injectable() @@ -12,11 +12,11 @@ export class OauthProviderLoginFlowService { constructor( private readonly ltiToolService: LtiToolService, private readonly externalToolService: ExternalToolService, - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures + private readonly configService: ConfigService ) {} public async findToolByClientId(clientId: string): Promise { - if (this.toolFeatures.ctlToolsTabEnabled) { + if (this.configService.get('FEATURE_CTL_TOOLS_TAB_ENABLED')) { const externalTool: ExternalTool | null = await this.externalToolService.findExternalToolByOAuth2ConfigClientId( clientId ); diff --git a/apps/server/src/modules/provisioning/config/index.ts b/apps/server/src/modules/provisioning/config/index.ts deleted file mode 100644 index dbbb1de579b..00000000000 --- a/apps/server/src/modules/provisioning/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ProvisioningFeatures, ProvisioningConfiguration, IProvisioningFeatures } from './provisioning-config'; diff --git a/apps/server/src/modules/provisioning/config/provisioning-config.ts b/apps/server/src/modules/provisioning/config/provisioning-config.ts deleted file mode 100644 index 1fa9b078d66..00000000000 --- a/apps/server/src/modules/provisioning/config/provisioning-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; - -export const ProvisioningFeatures = Symbol('ProvisioningFeatures'); - -export interface IProvisioningFeatures { - schulconnexGroupProvisioningEnabled: boolean; - schulconnexCourseSyncEnabled: boolean; - schulconnexOtherGroupusersEnabled: boolean; -} - -export class ProvisioningConfiguration { - static provisioningFeatures: IProvisioningFeatures = { - schulconnexGroupProvisioningEnabled: Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') as boolean, - schulconnexCourseSyncEnabled: Configuration.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') as boolean, - schulconnexOtherGroupusersEnabled: Configuration.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED') as boolean, - }; -} diff --git a/apps/server/src/modules/provisioning/provisioning-config.module.ts b/apps/server/src/modules/provisioning/provisioning-config.module.ts deleted file mode 100644 index 2e1aad944e9..00000000000 --- a/apps/server/src/modules/provisioning/provisioning-config.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ProvisioningConfiguration, ProvisioningFeatures } from './config'; - -@Module({ - providers: [ - { - provide: ProvisioningFeatures, - useValue: ProvisioningConfiguration.provisioningFeatures, - }, - ], - exports: [ProvisioningFeatures], -}) -export class ProvisioningConfigModule {} diff --git a/apps/server/src/modules/provisioning/provisioning.config.ts b/apps/server/src/modules/provisioning/provisioning.config.ts index 45586873b17..4d7906e59c6 100644 --- a/apps/server/src/modules/provisioning/provisioning.config.ts +++ b/apps/server/src/modules/provisioning/provisioning.config.ts @@ -2,4 +2,6 @@ export interface ProvisioningConfig { FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: boolean; FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED: boolean; PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL: string; + FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: boolean; + FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: boolean; } diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 152636a3c1c..6d65266a0a6 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -11,7 +11,6 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { SchulconnexClientModule } from '@src/infra/schulconnex-client'; import { UserLicenseModule } from '../user-license'; -import { ProvisioningConfigModule } from './provisioning-config.module'; import { ProvisioningService } from './service/provisioning.service'; import { IservProvisioningStrategy, @@ -30,7 +29,6 @@ import { @Module({ imports: [ - ProvisioningConfigModule, AccountModule, LegacySchoolModule, UserModule, diff --git a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts index a74bf170322..30408619f57 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts @@ -14,7 +14,6 @@ import { legacySchoolDoFactory, userDoFactory, } from '@shared/testing'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, ExternalSchoolDto, @@ -50,7 +49,6 @@ describe(SchulconnexProvisioningStrategy.name, () => { let module: TestingModule; let strategy: TestSchulconnexStrategy; - let provisioningFeatures: IProvisioningFeatures; let schulconnexSchoolProvisioningService: DeepMocked; let schulconnexUserProvisioningService: DeepMocked; let schulconnexGroupProvisioningService: DeepMocked; @@ -60,14 +58,12 @@ describe(SchulconnexProvisioningStrategy.name, () => { let configService: DeepMocked>; let schulconnexToolProvisioningService: DeepMocked; + const config: Partial = {}; + beforeAll(async () => { module = await Test.createTestingModule({ providers: [ TestSchulconnexStrategy, - { - provide: ProvisioningFeatures, - useValue: {}, - }, { provide: SchulconnexSchoolProvisioningService, useValue: createMock(), @@ -97,14 +93,15 @@ describe(SchulconnexProvisioningStrategy.name, () => { useValue: createMock(), }, { - provide: ConfigService, - useValue: createMock>(), + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), + }, }, ], }).compile(); strategy = module.get(TestSchulconnexStrategy); - provisioningFeatures = module.get(ProvisioningFeatures); schulconnexSchoolProvisioningService = module.get(SchulconnexSchoolProvisioningService); schulconnexUserProvisioningService = module.get(SchulconnexUserProvisioningService); schulconnexGroupProvisioningService = module.get(SchulconnexGroupProvisioningService); @@ -116,10 +113,9 @@ describe(SchulconnexProvisioningStrategy.name, () => { }); beforeEach(() => { - Object.assign>(provisioningFeatures, { - schulconnexGroupProvisioningEnabled: false, - schulconnexCourseSyncEnabled: false, - }); + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = false; + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; }); afterAll(async () => { @@ -127,7 +123,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe('apply is called', () => { @@ -246,7 +242,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when group data is provided and the feature is enabled', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; const externalUserId = 'externalUserId'; const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); @@ -306,7 +302,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when group data is provided, but the feature is disabled', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = false; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; const externalUserId = 'externalUserId'; const oauthData: OauthDataDto = new OauthDataDto({ @@ -350,7 +346,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when group data is not provided', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; const externalUserId = 'externalUserId'; const oauthData: OauthDataDto = new OauthDataDto({ @@ -391,8 +387,8 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when an existing group gets provisioned', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; - provisioningFeatures.schulconnexCourseSyncEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = true; const externalUserId = 'externalUserId'; const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); @@ -441,8 +437,8 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when a new group is provisioned', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; - provisioningFeatures.schulconnexCourseSyncEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = true; const externalUserId = 'externalUserId'; const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); @@ -486,8 +482,8 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when a user was removed from a group', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; - provisioningFeatures.schulconnexCourseSyncEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = true; const externalUserId = 'externalUserId'; const oauthData: OauthDataDto = new OauthDataDto({ diff --git a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts index e976afb7bc5..007b70319bc 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts @@ -1,8 +1,7 @@ import { Group, GroupService } from '@modules/group'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, OauthDataDto, ProvisioningDto } from '../../dto'; import { ProvisioningConfig } from '../../provisioning.config'; import { ProvisioningStrategy } from '../base.strategy'; @@ -18,7 +17,6 @@ import { @Injectable() export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrategy { constructor( - @Inject(ProvisioningFeatures) protected readonly provisioningFeatures: IProvisioningFeatures, protected readonly schulconnexSchoolProvisioningService: SchulconnexSchoolProvisioningService, protected readonly schulconnexUserProvisioningService: SchulconnexUserProvisioningService, protected readonly schulconnexGroupProvisioningService: SchulconnexGroupProvisioningService, @@ -46,7 +44,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate school?.id ); - if (this.provisioningFeatures.schulconnexGroupProvisioningEnabled) { + if (this.configService.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { await this.provisionGroups(data, school); } @@ -87,7 +85,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate data.system.systemId ); - if (this.provisioningFeatures.schulconnexCourseSyncEnabled && provisionedGroup) { + if (this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') && provisionedGroup) { await this.schulconnexCourseSyncService.synchronizeCourseWithGroup( provisionedGroup, existingGroup ?? undefined @@ -108,7 +106,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate data.system.systemId ); - if (this.provisioningFeatures.schulconnexCourseSyncEnabled) { + if (this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) { const courseSyncPromises: Promise[] = removedFromGroups.map( async (removedFromGroup: Group): Promise => { await this.schulconnexCourseSyncService.synchronizeCourseWithGroup(removedFromGroup, removedFromGroup); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts index 5b380116894..58d54a44495 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -17,7 +17,6 @@ import { ValidationErrorLoggableException } from '@shared/common/loggable-except import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import * as classValidator from 'class-validator'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, ExternalLicenseDto, @@ -52,9 +51,8 @@ describe(SanisProvisioningStrategy.name, () => { ArgsType >; - let provisioningFeatures: IProvisioningFeatures; - let configService: DeepMocked>; let schulconnexRestClient: DeepMocked; + const config: Partial = {}; beforeAll(async () => { module = await Test.createTestingModule({ @@ -92,13 +90,11 @@ describe(SanisProvisioningStrategy.name, () => { provide: SchulconnexToolProvisioningService, useValue: createMock(), }, - { - provide: ProvisioningFeatures, - useValue: {}, - }, { provide: ConfigService, - useValue: createMock>(), + useValue: { + get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), + }, }, { provide: SchulconnexRestClient, @@ -109,21 +105,16 @@ describe(SanisProvisioningStrategy.name, () => { strategy = module.get(SanisProvisioningStrategy); mapper = module.get(SchulconnexResponseMapper); - provisioningFeatures = module.get(ProvisioningFeatures); schulconnexRestClient = module.get(SchulconnexRestClient); - configService = module.get(ConfigService); validationFunction = jest.spyOn(classValidator, 'validate'); }); - beforeEach(() => { - Object.assign>(provisioningFeatures, { - schulconnexGroupProvisioningEnabled: true, - }); - }); - afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); + validationFunction.mockReset(); + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = false; }); const setupSchulconnexResponse = (): SchulconnexResponse => schulconnexResponseFactory.build(); @@ -179,13 +170,14 @@ describe(SanisProvisioningStrategy.name, () => { schulconnexLizenzInfoResponse, ]); + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = true; schulconnexRestClient.getPersonInfo.mockResolvedValueOnce(schulconnexResponse); mapper.mapToExternalUserDto.mockReturnValue(user); mapper.mapToExternalSchoolDto.mockReturnValue(school); mapper.mapToExternalGroupDtos.mockReturnValue(groups); validationFunction.mockResolvedValueOnce([]); validationFunction.mockResolvedValueOnce([]); - configService.get.mockReturnValueOnce(true); schulconnexRestClient.getLizenzInfo.mockResolvedValueOnce(schulconnexLizenzInfoResponses); validationFunction.mockResolvedValueOnce([]); @@ -287,7 +279,7 @@ describe(SanisProvisioningStrategy.name, () => { mapper.mapToExternalSchoolDto.mockReturnValue(school); validationFunction.mockResolvedValueOnce([]); - provisioningFeatures.schulconnexGroupProvisioningEnabled = false; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; return { input, @@ -342,7 +334,7 @@ describe(SanisProvisioningStrategy.name, () => { validationFunction.mockResolvedValueOnce([]); validationFunction.mockResolvedValueOnce([]); - configService.get.mockReturnValueOnce(false); + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = false; return { input, diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index 760f93218e1..e55305ade14 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -5,14 +5,13 @@ import { } from '@infra/schulconnex-client/response'; import { SchulconnexRestClient } from '@infra/schulconnex-client/schulconnex-rest-client'; import { GroupService } from '@modules/group/service/group.service'; -import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ValidationErrorLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, ExternalLicenseDto, @@ -36,7 +35,6 @@ import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; @Injectable() export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { constructor( - @Inject(ProvisioningFeatures) protected readonly provisioningFeatures: IProvisioningFeatures, protected readonly schulconnexSchoolProvisioningService: SchulconnexSchoolProvisioningService, protected readonly schulconnexUserProvisioningService: SchulconnexUserProvisioningService, protected readonly schulconnexGroupProvisioningService: SchulconnexGroupProvisioningService, @@ -49,7 +47,6 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { private readonly schulconnexRestClient: SchulconnexRestClient ) { super( - provisioningFeatures, schulconnexSchoolProvisioningService, schulconnexUserProvisioningService, schulconnexGroupProvisioningService, @@ -92,7 +89,7 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { const externalSchool: ExternalSchoolDto = this.responseMapper.mapToExternalSchoolDto(schulconnexResponse); let externalGroups: ExternalGroupDto[] | undefined; - if (this.provisioningFeatures.schulconnexGroupProvisioningEnabled) { + if (this.configService.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { await this.checkResponseValidation(schulconnexResponse, [SchulconnexResponseValidationGroups.GROUPS]); externalGroups = this.responseMapper.mapToExternalGroupDtos(schulconnexResponse); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts index 5a8bc7d0a15..4173c8d2ed0 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts @@ -11,19 +11,20 @@ import { SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client'; import { GroupTypes } from '@modules/group'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { InvalidLaufzeitResponseLoggableException, InvalidLernperiodeResponseLoggableException } from '../../domain'; import { ExternalGroupDto, ExternalLicenseDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; +import { ProvisioningConfig } from '../../provisioning.config'; import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; describe(SchulconnexResponseMapper.name, () => { let module: TestingModule; let mapper: SchulconnexResponseMapper; - let provisioningFeatures: IProvisioningFeatures; + const config: Partial = {}; beforeAll(async () => { module = await Test.createTestingModule({ @@ -34,14 +35,15 @@ describe(SchulconnexResponseMapper.name, () => { useValue: createMock(), }, { - provide: ProvisioningFeatures, - useValue: {}, + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), + }, }, ], }).compile(); mapper = module.get(SchulconnexResponseMapper); - provisioningFeatures = module.get(ProvisioningFeatures); }); describe('mapToExternalSchoolDto', () => { @@ -143,9 +145,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when group type class is given', () => { const setup = () => { - Object.assign>(provisioningFeatures, { - schulconnexOtherGroupusersEnabled: true, - }); + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); @@ -274,9 +274,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when no other participants are provided and FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED is false', () => { const setup = () => { - Object.assign>(provisioningFeatures, { - schulconnexOtherGroupusersEnabled: false, - }); + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = false; const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0].sonstige_gruppenzugehoerige = undefined; @@ -296,9 +294,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when no other participants are provided and FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED is true', () => { const setup = () => { - Object.assign>(provisioningFeatures, { - schulconnexOtherGroupusersEnabled: true, - }); + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0].sonstige_gruppenzugehoerige = undefined; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts index 7e66f6d2900..34a31f9a55e 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts @@ -12,10 +12,10 @@ import { SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client/response'; import { GroupTypes } from '@modules/group'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { InvalidLaufzeitResponseLoggableException, InvalidLernperiodeResponseLoggableException } from '../../domain'; import { ExternalGroupDto, @@ -25,6 +25,7 @@ import { ExternalUserDto, } from '../../dto'; import { GroupRoleUnknownLoggable } from '../../loggable'; +import { ProvisioningConfig } from '../../provisioning.config'; const RoleMapping: Record = { [SchulconnexRole.LEHR]: RoleName.TEACHER, @@ -54,7 +55,7 @@ export class SchulconnexResponseMapper { SCHOOLNUMBER_PREFIX_REGEX = /^NI_/; constructor( - @Inject(ProvisioningFeatures) protected readonly provisioningFeatures: IProvisioningFeatures, + private readonly configService: ConfigService, private readonly logger: Logger ) {} @@ -141,7 +142,7 @@ export class SchulconnexResponseMapper { } let otherUsers: ExternalGroupUserDto[] | undefined; - if (this.provisioningFeatures.schulconnexOtherGroupusersEnabled) { + if (this.configService.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED')) { otherUsers = group.sonstige_gruppenzugehoerige ? (group.sonstige_gruppenzugehoerige .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index 3110d7761f4..9fe1276ab7f 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -244,7 +244,7 @@ export class ConfigResponse { this.FEATURE_LESSON_SHARE = config.FEATURE_LESSON_SHARE; this.FEATURE_TASK_SHARE = config.FEATURE_TASK_SHARE; this.FEATURE_BOARD_LAYOUT_ENABLED = config.FEATURE_BOARD_LAYOUT_ENABLED; - this.FEATURE_USER_MIGRATION_ENABLED = config.userMigrationEnabled; + this.FEATURE_USER_MIGRATION_ENABLED = config.FEATURE_USER_MIGRATION_ENABLED; this.FEATURE_COPY_SERVICE_ENABLED = config.FEATURE_COPY_SERVICE_ENABLED; this.FEATURE_CONSENT_NECESSARY = config.FEATURE_CONSENT_NECESSARY; this.FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED = config.FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED; @@ -266,20 +266,20 @@ export class ConfigResponse { this.FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED = config.FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED; this.MIGRATION_END_GRACE_PERIOD_MS = config.MIGRATION_END_GRACE_PERIOD_MS; - this.FEATURE_CTL_TOOLS_TAB_ENABLED = config.ctlToolsTabEnabled; - this.FEATURE_LTI_TOOLS_TAB_ENABLED = config.ltiToolsTabEnabled; + this.FEATURE_CTL_TOOLS_TAB_ENABLED = config.FEATURE_CTL_TOOLS_TAB_ENABLED; + this.FEATURE_LTI_TOOLS_TAB_ENABLED = config.FEATURE_LTI_TOOLS_TAB_ENABLED; this.FEATURE_SHOW_OUTDATED_USERS = config.FEATURE_SHOW_OUTDATED_USERS; this.FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION = config.FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION; - this.CTL_TOOLS_RELOAD_TIME_MS = config.ctlToolsReloadTimeMs; + this.CTL_TOOLS_RELOAD_TIME_MS = config.CTL_TOOLS_RELOAD_TIME_MS; this.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED = config.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED; - this.FEATURE_CTL_TOOLS_COPY_ENABLED = config.ctlToolsCopyEnabled; + this.FEATURE_CTL_TOOLS_COPY_ENABLED = config.FEATURE_CTL_TOOLS_COPY_ENABLED; this.FEATURE_SHOW_MIGRATION_WIZARD = config.FEATURE_SHOW_MIGRATION_WIZARD; this.MIGRATION_WIZARD_DOCUMENTATION_LINK = config.MIGRATION_WIZARD_DOCUMENTATION_LINK; this.FEATURE_TLDRAW_ENABLED = config.FEATURE_TLDRAW_ENABLED; this.TLDRAW__ASSETS_ENABLED = config.TLDRAW__ASSETS_ENABLED; this.TLDRAW__ASSETS_MAX_SIZE = config.TLDRAW__ASSETS_MAX_SIZE; this.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST = config.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST; - this.FEATURE_VIDEOCONFERENCE_ENABLED = config.enabled; + this.FEATURE_VIDEOCONFERENCE_ENABLED = config.FEATURE_VIDEOCONFERENCE_ENABLED; this.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED; this.FEATURE_MEDIA_SHELF_ENABLED = config.FEATURE_MEDIA_SHELF_ENABLED; this.BOARD_COLLABORATION_URI = config.BOARD_COLLABORATION_URI; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index a5d2b6ddf45..3b55a49ee4a 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -16,16 +16,16 @@ import { ProvisioningConfig } from '@modules/provisioning'; import type { SchoolConfig } from '@modules/school'; import type { SharingConfig } from '@modules/sharing'; import { getTldrawClientConfig, type TldrawClientConfig } from '@modules/tldraw-client'; -import { ToolConfiguration, type IToolFeatures } from '@modules/tool'; +import type { ToolConfig } from '@modules/tool/tool-config'; import type { UserConfig } from '@modules/user'; -import { UserImportConfiguration, type IUserImportFeatures } from '@modules/user-import'; +import type { UserImportConfig } from '@modules/user-import'; import type { UserLoginMigrationConfig } from '@modules/user-login-migration'; -import { VideoConferenceConfiguration, type IVideoConferenceSettings } from '@modules/video-conference'; +import { VideoConferenceConfig } from '@modules/video-conference'; import { LanguageType } from '@shared/domain/interface'; import { SchulcloudTheme } from '@shared/domain/types'; import type { CoreModuleConfig } from '@src/core'; import type { MailConfig } from '@src/infra/mail/interfaces/mail-config'; -import { UserImportConfig } from '../user-import/user-import-config'; +import { BbbConfig } from '../video-conference/bbb'; import { Timezone } from './types/timezone.enum'; export enum NodeEnvType { @@ -48,24 +48,26 @@ export interface ServerConfig XApiKeyConfig, LearnroomConfig, AuthenticationConfig, - IToolFeatures, + ToolConfig, TldrawClientConfig, UserLoginMigrationConfig, LessonConfig, - IVideoConferenceSettings, BoardConfig, MediaBoardConfig, SharingConfig, - IUserImportFeatures, + UserImportConfig, SchulconnexClientConfig, SynchronizationConfig, DeletionConfig, CollaborativeTextEditorConfig, ProvisioningConfig, UserImportConfig, + VideoConferenceConfig, + BbbConfig, AlertConfig { NODE_ENV: NodeEnvType; SC_DOMAIN: string; + HOST: string; ACCESSIBILITY_REPORT_EMAIL: string; ADMIN_TABLES_DISPLAY_CONSENT_COLUMN: boolean; ALERT_STATUS_URL: string | null; @@ -254,9 +256,6 @@ const config: ServerConfig = { ) as number, FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: Configuration.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') as boolean, ...getTldrawClientConfig(), - ...ToolConfiguration.toolFeatures, - ...VideoConferenceConfiguration.videoConference, - ...UserImportConfiguration.userImportFeatures, FEATURE_MEDIA_SHELF_ENABLED: Configuration.get('FEATURE_MEDIA_SHELF_ENABLED') as boolean, FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: Configuration.get( 'FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED' @@ -266,6 +265,25 @@ const config: ServerConfig = { PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL: Configuration.get('PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL') as string, BOARD_COLLABORATION_URI: Configuration.get('BOARD_COLLABORATION_URI') as string, FEATURE_NEW_LAYOUT_ENABLED: Configuration.get('FEATURE_NEW_LAYOUT_ENABLED') as boolean, + FEATURE_CTL_TOOLS_TAB_ENABLED: Configuration.get('FEATURE_CTL_TOOLS_TAB_ENABLED') as boolean, + FEATURE_LTI_TOOLS_TAB_ENABLED: Configuration.get('FEATURE_LTI_TOOLS_TAB_ENABLED') as boolean, + CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES: Configuration.get( + 'CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES' + ) as number, + CTL_TOOLS_BACKEND_URL: Configuration.get('PUBLIC_BACKEND_URL') as string, + FEATURE_CTL_TOOLS_COPY_ENABLED: Configuration.get('FEATURE_CTL_TOOLS_COPY_ENABLED') as boolean, + CTL_TOOLS_RELOAD_TIME_MS: Configuration.get('CTL_TOOLS_RELOAD_TIME_MS') as number, + HOST: Configuration.get('HOST') as string, + FEATURE_VIDEOCONFERENCE_ENABLED: Configuration.get('FEATURE_VIDEOCONFERENCE_ENABLED') as boolean, + VIDEOCONFERENCE_HOST: Configuration.get('VIDEOCONFERENCE_HOST') as string, + VIDEOCONFERENCE_SALT: Configuration.get('VIDEOCONFERENCE_SALT') as string, + VIDEOCONFERENCE_DEFAULT_PRESENTATION: Configuration.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION') as string, + FEATURE_USER_MIGRATION_ENABLED: Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean, + FEATURE_USER_MIGRATION_SYSTEM_ID: Configuration.get('FEATURE_USER_MIGRATION_SYSTEM_ID') as string, + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: Configuration.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ) as boolean, + FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') as boolean, }; export const serverConfig = () => config; diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index b28498f9b1e..ed1060ec737 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -8,6 +8,7 @@ import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { AccountApiModule } from '@modules/account/account-api.module'; import { AlertModule } from '@modules/alert/alert.module'; import { AuthenticationApiModule } from '@modules/authentication/authentication-api.module'; +import { AuthorizationReferenceApiModule } from '@modules/authorization/authorization-reference.api.module'; import { BoardApiModule } from '@modules/board/board-api.module'; import { MediaBoardApiModule } from '@modules/board/media-board-api.module'; import { CollaborativeStorageModule } from '@modules/collaborative-storage'; @@ -30,7 +31,8 @@ import { SystemApiModule } from '@modules/system/system-api.module'; import { TaskApiModule } from '@modules/task/task-api.module'; import { TeamsApiModule } from '@modules/teams/teams-api.module'; import { ToolApiModule } from '@modules/tool/tool-api.module'; -import { ImportUserModule, UserImportConfigModule } from '@modules/user-import'; +import { ImportUserModule } from '@modules/user-import'; +import { UserLicenseModule } from '@modules/user-license'; import { UserLoginMigrationApiModule } from '@modules/user-login-migration/user-login-migration-api.module'; import { UsersAdminApiModule } from '@modules/user/legacy/users-admin-api.module'; import { UserApiModule } from '@modules/user/user-api.module'; @@ -41,8 +43,6 @@ import { ALL_ENTITIES } from '@shared/domain/entity'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { UserLicenseModule } from '@modules/user-license'; -import { AuthorizationReferenceApiModule } from '@modules/authorization/authorization-reference.api.module'; import { ServerConfigController, ServerController, ServerUc } from './api'; import { SERVER_CONFIG_TOKEN, serverConfig } from './server.config'; @@ -62,7 +62,6 @@ const serverModules = [ UsersAdminApiModule, SchulconnexClientModule.registerAsync(), ImportUserModule, - UserImportConfigModule, LearnroomApiModule, FilesStorageClientModule, SystemApiModule, 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 563118a175d..35b04c2d3b4 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 @@ -4,7 +4,6 @@ import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; -import { ToolConfigModule } from '../tool-config.module'; import { ContextExternalToolAuthorizableService, ContextExternalToolService, ToolReferenceService } from './service'; import { ContextExternalToolValidationService } from './service/context-external-tool-validation.service'; import { ToolConfigurationStatusService } from './service/tool-configuration-status.service'; @@ -15,7 +14,6 @@ import { ToolConfigurationStatusService } from './service/tool-configuration-sta forwardRef(() => ExternalToolModule), SchoolExternalToolModule, LoggerModule, - ToolConfigModule, UserLicenseModule, ], providers: [ 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 c7273f73f0e..20d96485a9a 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 @@ -6,7 +6,6 @@ import { ExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ToolContextMapper } from '../common/mapper/tool-context.mapper'; -import { ToolConfigModule } from '../tool-config.module'; import { ExternalToolMetadataMapper } from './mapper'; import { DatasheetPdfService, @@ -20,7 +19,7 @@ import { } from './service'; @Module({ - imports: [CommonToolModule, ToolConfigModule, LoggerModule, OauthProviderServiceModule, EncryptionModule, HttpModule], + imports: [CommonToolModule, LoggerModule, OauthProviderServiceModule, EncryptionModule, HttpModule], providers: [ ExternalToolService, ExternalToolServiceMapper, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts index 4afa83fe301..d6240b1c242 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts @@ -11,7 +11,6 @@ import { schoolExternalToolConfigurationStatusFactory, schoolExternalToolFactory, } from '../../school-external-tool/testing'; -import { ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; import { customParameterFactory, externalToolFactory } from '../testing'; import { ContextExternalToolTemplateInfo } from '../uc'; @@ -28,12 +27,6 @@ describe('ExternalToolConfigurationService', () => { module = await Test.createTestingModule({ providers: [ ExternalToolConfigurationService, - { - provide: ToolFeatures, - useValue: { - contextConfigurationEnabled: false, - }, - }, { provide: CommonToolService, useValue: createMock(), diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.spec.ts similarity index 95% rename from apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts rename to apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.spec.ts index 40d7185ac98..c374e1362d0 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.spec.ts @@ -1,11 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { axiosResponseFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { of, throwError } from 'rxjs'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { @@ -19,13 +20,13 @@ import { externalToolFactory } from '../testing'; import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolService } from './external-tool.service'; -describe('ExternalToolLogoService', () => { +describe(ExternalToolLogoService.name, () => { let module: TestingModule; let service: ExternalToolLogoService; let httpService: DeepMocked; let logger: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; let externalToolService: DeepMocked; beforeAll(async () => { @@ -41,10 +42,8 @@ describe('ExternalToolLogoService', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - maxExternalToolLogoSizeInBytes: 30000, - }, + provide: ConfigService, + useValue: createMock>(), }, { provide: ExternalToolService, @@ -56,7 +55,7 @@ describe('ExternalToolLogoService', () => { service = module.get(ExternalToolLogoService); httpService = module.get(HttpService); logger = module.get(Logger); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); externalToolService = module.get(ExternalToolService); }); @@ -91,7 +90,8 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); - const baseUrl = toolFeatures.backEndUrl; + const baseUrl = 'https://backend.com'; + configService.get.mockReturnValue(baseUrl); const { id } = externalTool; const expected = `${baseUrl}/v3/tools/external-tools/${id}/logo`; @@ -116,7 +116,7 @@ describe('ExternalToolLogoService', () => { describe('when size is exceeded', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 1; + configService.get.mockReturnValue(1); return { externalTool }; }; @@ -133,7 +133,7 @@ describe('ExternalToolLogoService', () => { describe('when size is not exceeded', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 30000; + configService.get.mockReturnValue(30000); return { externalTool }; }; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts index 2a274f1a17d..01c4689ad53 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts @@ -1,10 +1,11 @@ import { HttpService } from '@nestjs/axios'; -import { HttpException, Inject } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import { lastValueFrom } from 'rxjs'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { @@ -23,17 +24,18 @@ const contentTypeDetector: Record = { '47494638': 'image/gif', }; +@Injectable() export class ExternalToolLogoService { constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly configService: ConfigService, private readonly logger: Logger, private readonly httpService: HttpService, private readonly externalToolService: ExternalToolService ) {} - buildLogoUrl(externalTool: ExternalTool): string | undefined { + public buildLogoUrl(externalTool: ExternalTool): string | undefined { const { logo, id } = externalTool; - const backendUrl = this.toolFeatures.backEndUrl; + const backendUrl = this.configService.get('CTL_TOOLS_BACKEND_URL'); if (logo && id) { return `${backendUrl}/v3/tools/external-tools/${id}/logo`; @@ -42,22 +44,22 @@ export class ExternalToolLogoService { return undefined; } - validateLogoSize(externalTool: Partial): void { + public validateLogoSize(externalTool: Partial): void { if (!externalTool.logo) { return; } const buffer: Buffer = Buffer.from(externalTool.logo, 'base64'); - if (buffer.length > this.toolFeatures.maxExternalToolLogoSizeInBytes) { + if (buffer.length > this.configService.get('CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES')) { throw new ExternalToolLogoSizeExceededLoggableException( externalTool.id, - this.toolFeatures.maxExternalToolLogoSizeInBytes + this.configService.get('CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES') ); } } - async fetchLogo(externalTool: Partial): Promise { + public async fetchLogo(externalTool: Partial): Promise { if (externalTool.logoUrl) { const base64Logo: string = await this.fetchBase64Logo(externalTool.logoUrl); @@ -93,7 +95,7 @@ export class ExternalToolLogoService { } } - async getExternalToolBinaryLogo(toolId: EntityId): Promise { + public async getExternalToolBinaryLogo(toolId: EntityId): Promise { const tool: ExternalTool = await this.externalToolService.findById(toolId); if (!tool.logo) { diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts index 7216a57414e..ae618ae084a 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts @@ -1,7 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { externalToolFactory } from '../testing'; import { ExternalToolLogoService } from './external-tool-logo.service'; @@ -9,13 +10,13 @@ import { ExternalToolParameterValidationService } from './external-tool-paramete import { ExternalToolValidationService } from './external-tool-validation.service'; import { ExternalToolService } from './external-tool.service'; -describe('ExternalToolValidationService', () => { +describe(ExternalToolValidationService.name, () => { let module: TestingModule; let service: ExternalToolValidationService; let externalToolService: DeepMocked; let commonToolValidationService: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; let logoService: DeepMocked; beforeAll(async () => { @@ -31,10 +32,8 @@ describe('ExternalToolValidationService', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - maxExternalToolLogoSizeInBytes: 30000, - }, + provide: ConfigService, + useValue: createMock>(), }, { provide: ExternalToolLogoService, @@ -46,7 +45,7 @@ describe('ExternalToolValidationService', () => { service = module.get(ExternalToolValidationService); externalToolService = module.get(ExternalToolService); commonToolValidationService = module.get(ExternalToolParameterValidationService); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); logoService = module.get(ExternalToolLogoService); }); @@ -189,7 +188,7 @@ describe('ExternalToolValidationService', () => { describe('when external tool has a given base64 logo', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 30000; + configService.get.mockReturnValue(30000); return { externalTool }; }; @@ -362,7 +361,7 @@ describe('ExternalToolValidationService', () => { describe('when external tool has a given base64 logo', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 30000; + configService.get.mockReturnValue(30000); return { externalTool }; }; diff --git a/apps/server/src/modules/tool/index.ts b/apps/server/src/modules/tool/index.ts index f6ba7329778..a8006029057 100644 --- a/apps/server/src/modules/tool/index.ts +++ b/apps/server/src/modules/tool/index.ts @@ -2,5 +2,4 @@ export * from './common/interface'; export * from './context-external-tool/service/context-external-tool-authorizable.service'; export * from './external-tool'; export * from './tool.module'; -export { default as ToolConfiguration, IToolFeatures } from './tool-config'; export { ExternalToolAuthorizableService } from './external-tool/service/external-tool-authorizable.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 2ff671cb62b..8d183c1f88b 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 @@ -1,11 +1,10 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; -import { ToolConfigModule } from '../tool-config.module'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from './service'; @Module({ - imports: [forwardRef(() => CommonToolModule), forwardRef(() => ExternalToolModule), ToolConfigModule], + imports: [forwardRef(() => CommonToolModule), forwardRef(() => ExternalToolModule)], providers: [SchoolExternalToolService, SchoolExternalToolValidationService], exports: [SchoolExternalToolService, SchoolExternalToolValidationService], }) diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 783c82ac3da..12d33bd3d90 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -19,7 +19,6 @@ import { ExternalToolConfigurationService } from './external-tool/service'; import { ExternalToolConfigurationUc, ExternalToolUc } from './external-tool/uc'; import { ToolSchoolController } from './school-external-tool/controller'; import { SchoolExternalToolUc } from './school-external-tool/uc'; -import { ToolConfigModule } from './tool-config.module'; import { ToolLaunchController } from './tool-launch/controller/tool-launch.controller'; import { ToolLaunchUc } from './tool-launch/uc'; import { ToolModule } from './tool.module'; @@ -32,7 +31,6 @@ import { ToolModule } from './tool.module'; AuthorizationModule, LoggerModule, LegacySchoolModule, - ToolConfigModule, LearnroomModule, BoardModule, SchoolModule, diff --git a/apps/server/src/modules/tool/tool-config.module.ts b/apps/server/src/modules/tool/tool-config.module.ts deleted file mode 100644 index f30458519f5..00000000000 --- a/apps/server/src/modules/tool/tool-config.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import ToolConfiguration, { ToolFeatures } from './tool-config'; - -@Module({ - providers: [ - { - provide: ToolFeatures, - useValue: ToolConfiguration.toolFeatures, - }, - ], - exports: [ToolFeatures], -}) -export class ToolConfigModule {} diff --git a/apps/server/src/modules/tool/tool-config.ts b/apps/server/src/modules/tool/tool-config.ts index ccd33a579ce..98ec436a322 100644 --- a/apps/server/src/modules/tool/tool-config.ts +++ b/apps/server/src/modules/tool/tool-config.ts @@ -1,23 +1,8 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; - -export const ToolFeatures = Symbol('ToolFeatures'); - -export interface IToolFeatures { - ctlToolsTabEnabled: boolean; - ltiToolsTabEnabled: boolean; - maxExternalToolLogoSizeInBytes: number; - backEndUrl: string; - ctlToolsCopyEnabled: boolean; - ctlToolsReloadTimeMs: number; -} - -export default class ToolConfiguration { - static toolFeatures: IToolFeatures = { - ctlToolsTabEnabled: Configuration.get('FEATURE_CTL_TOOLS_TAB_ENABLED') as boolean, - ltiToolsTabEnabled: Configuration.get('FEATURE_LTI_TOOLS_TAB_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, - ctlToolsCopyEnabled: Configuration.get('FEATURE_CTL_TOOLS_COPY_ENABLED') as boolean, - ctlToolsReloadTimeMs: Configuration.get('CTL_TOOLS_RELOAD_TIME_MS') as number, - }; +export interface ToolConfig { + FEATURE_CTL_TOOLS_TAB_ENABLED: boolean; + FEATURE_LTI_TOOLS_TAB_ENABLED: boolean; + CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES: number; + CTL_TOOLS_BACKEND_URL: string; + FEATURE_CTL_TOOLS_COPY_ENABLED: boolean; + CTL_TOOLS_RELOAD_TIME_MS: number; } diff --git a/apps/server/src/modules/tool/tool.module.ts b/apps/server/src/modules/tool/tool.module.ts index 91a19c5c995..b0ba0d429fe 100644 --- a/apps/server/src/modules/tool/tool.module.ts +++ b/apps/server/src/modules/tool/tool.module.ts @@ -1,15 +1,13 @@ import { forwardRef, Module } from '@nestjs/common'; +import { CommonToolModule } from './common'; +import { CommonToolService } from './common/service'; import { ContextExternalToolModule } from './context-external-tool'; -import { SchoolExternalToolModule } from './school-external-tool'; import { ExternalToolModule } from './external-tool'; -import { CommonToolModule } from './common'; +import { SchoolExternalToolModule } from './school-external-tool'; import { ToolLaunchModule } from './tool-launch'; -import { CommonToolService } from './common/service'; -import { ToolConfigModule } from './tool-config.module'; @Module({ imports: [ - ToolConfigModule, forwardRef(() => CommonToolModule), ExternalToolModule, SchoolExternalToolModule, diff --git a/apps/server/src/modules/user-import/config/index.ts b/apps/server/src/modules/user-import/config/index.ts deleted file mode 100644 index 3267752a160..00000000000 --- a/apps/server/src/modules/user-import/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UserImportFeatures, UserImportConfiguration, IUserImportFeatures } from './user-import-config'; diff --git a/apps/server/src/modules/user-import/config/user-import-config.ts b/apps/server/src/modules/user-import/config/user-import-config.ts deleted file mode 100644 index 05cd6265a87..00000000000 --- a/apps/server/src/modules/user-import/config/user-import-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; - -export const UserImportFeatures = Symbol('UserImportFeatures'); - -export interface IUserImportFeatures { - userMigrationEnabled: boolean; - userMigrationSystemId: string; - useWithUserLoginMigration: boolean; -} - -export class UserImportConfiguration { - static userImportFeatures: IUserImportFeatures = { - userMigrationEnabled: Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean, - userMigrationSystemId: Configuration.get('FEATURE_USER_MIGRATION_SYSTEM_ID') as string, - useWithUserLoginMigration: Configuration.get('FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION') as boolean, - }; -} diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts index 2da305760de..21b9b6b005a 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts @@ -1,6 +1,7 @@ import { SchulconnexResponse, schulconnexResponseFactory } from '@infra/schulconnex-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { serverConfig, ServerConfig } from '@modules/server'; import { ServerTestModule } from '@modules/server/server.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -10,14 +11,12 @@ import { roleFactory, schoolEntityFactory, systemEntityFactory, TestApiClient, u import { accountFactory } from '@src/modules/account/testing'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { IUserImportFeatures, UserImportFeatures } from '../../config'; describe('ImportUser Controller Populate (API)', () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; - let userImportFeatures: IUserImportFeatures; let axiosMock: MockAdapter; const authenticatedUser = async ( @@ -42,9 +41,10 @@ describe('ImportUser Controller Populate (API)', () => { }; const setConfig = (systemId?: string) => { - userImportFeatures.userMigrationEnabled = true; - userImportFeatures.userMigrationSystemId = systemId || new ObjectId().toString(); - userImportFeatures.useWithUserLoginMigration = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = systemId || new ObjectId().toString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }; beforeAll(async () => { @@ -58,7 +58,6 @@ describe('ImportUser Controller Populate (API)', () => { em = app.get(EntityManager); testApiClient = new TestApiClient(app, 'user/import'); - userImportFeatures = app.get(UserImportFeatures); axiosMock = new MockAdapter(axios); }); @@ -90,7 +89,8 @@ describe('ImportUser Controller Populate (API)', () => { const { account } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE]); const loggedInClient = await testApiClient.login(account); - userImportFeatures.userMigrationEnabled = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = false; return { loggedInClient }; }; @@ -113,7 +113,8 @@ describe('ImportUser Controller Populate (API)', () => { const setup = async () => { const { account, school, system } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE], [], false); const loggedInClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system.id; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system.id; school.externalId = undefined; @@ -139,8 +140,9 @@ describe('ImportUser Controller Populate (API)', () => { const { account, school, system } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE]); const loggedInClient = await testApiClient.login(account); - userImportFeatures.userMigrationEnabled = true; - userImportFeatures.userMigrationSystemId = system.id; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system.id; axiosMock.onPost(/(.*)\/token/).reply(HttpStatus.OK, { id_token: 'idToken', diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 65cb8f82969..c0aa9b06e36 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -1,4 +1,5 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { serverConfig, ServerConfig } from '@modules/server'; import { ServerTestModule } from '@modules/server/server.module'; import { SystemEntity } from '@modules/system/entity'; import { @@ -35,14 +36,12 @@ import { } from '@shared/testing'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { accountFactory } from '@src/modules/account/testing'; -import { IUserImportFeatures, UserImportFeatures } from '../../config'; describe('ImportUser Controller (API)', () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; - let userImportFeatures: IUserImportFeatures; const authenticatedUser = async ( permissions: Permission[] = [], @@ -67,9 +66,10 @@ describe('ImportUser Controller (API)', () => { }; const setConfig = (systemId?: string) => { - userImportFeatures.userMigrationEnabled = true; - userImportFeatures.userMigrationSystemId = systemId || new ObjectId().toString(); - userImportFeatures.useWithUserLoginMigration = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = systemId || new ObjectId().toString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }; beforeAll(async () => { @@ -83,7 +83,6 @@ describe('ImportUser Controller (API)', () => { em = app.get(EntityManager); testApiClient = new TestApiClient(app, 'user/import'); - userImportFeatures = app.get(UserImportFeatures); }); afterAll(async () => { @@ -116,8 +115,11 @@ describe('ImportUser Controller (API)', () => { Permission.IMPORT_USER_VIEW, ])); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationEnabled = false; - userImportFeatures.userMigrationSystemId = ''; + + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = false; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = ''; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }); afterEach(() => { @@ -173,7 +175,8 @@ describe('ImportUser Controller (API)', () => { beforeEach(async () => { ({ account, system } = await authenticatedUser()); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); }); it('GET /user/import is UNAUTHORIZED', async () => { @@ -225,8 +228,9 @@ describe('ImportUser Controller (API)', () => { [SchoolFeature.LDAP_UNIVENTION_MIGRATION] )); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); - userImportFeatures.userMigrationEnabled = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = false; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); }); it('GET user/import is authorized, despite feature not enabled', async () => { @@ -244,7 +248,8 @@ describe('ImportUser Controller (API)', () => { beforeEach(async () => { ({ school, system, account } = await authenticatedUser([Permission.IMPORT_USER_VIEW])); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); }); it('GET /user/import responds with importusers', async () => { @@ -1082,7 +1087,8 @@ describe('ImportUser Controller (API)', () => { it('should set in user migration mode', async () => { ({ account, system } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE])); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); await testApiClient.post('startUserMigration').expect(HttpStatus.CREATED); }); diff --git a/apps/server/src/modules/user-import/index.ts b/apps/server/src/modules/user-import/index.ts index 0d666df6a20..e149f9d70bf 100644 --- a/apps/server/src/modules/user-import/index.ts +++ b/apps/server/src/modules/user-import/index.ts @@ -1,4 +1,3 @@ export { ImportUserModule } from './user-import.module'; -export { UserImportConfigModule } from './user-import-config.module'; -export { IUserImportFeatures, UserImportConfiguration } from './config'; +export { UserImportConfig } from './user-import-config'; export { UserImportService } from './service'; diff --git a/apps/server/src/modules/user-import/service/user-import.service.spec.ts b/apps/server/src/modules/user-import/service/user-import.service.spec.ts index 5575c423610..87d683f2868 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.spec.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.spec.ts @@ -5,6 +5,7 @@ import { LegacySchoolService } from '@modules/legacy-school'; import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; @@ -20,8 +21,8 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; +import { UserImportConfig } from '../user-import-config'; import { UserImportService } from './user-import.service'; describe(UserImportService.name, () => { @@ -35,10 +36,11 @@ describe(UserImportService.name, () => { let logger: DeepMocked; let schoolService: DeepMocked; - const features: IUserImportFeatures = { - userMigrationSystemId: new ObjectId().toHexString(), - userMigrationEnabled: true, - useWithUserLoginMigration: true, + const config: UserImportConfig = { + FEATURE_USER_MIGRATION_SYSTEM_ID: new ObjectId().toHexString(), + FEATURE_USER_MIGRATION_ENABLED: true, + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: true, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: 8000, }; beforeAll(async () => { @@ -48,6 +50,12 @@ describe(UserImportService.name, () => { imports: [MongoMemoryDatabaseModule.forRoot()], providers: [ UserImportService, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof UserImportConfig) => config[key]), + }, + }, { provide: ImportUserRepo, useValue: createMock(), @@ -60,10 +68,6 @@ describe(UserImportService.name, () => { provide: UserService, useValue: createMock(), }, - { - provide: UserImportFeatures, - useValue: features, - }, { provide: Logger, useValue: createMock(), @@ -138,7 +142,7 @@ describe(UserImportService.name, () => { describe('checkFeatureEnabled', () => { describe('when the global feature is enabled', () => { const setup = () => { - features.userMigrationEnabled = true; + config.FEATURE_USER_MIGRATION_ENABLED = true; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }); @@ -156,7 +160,7 @@ describe(UserImportService.name, () => { describe('when the school feature is enabled', () => { const setup = () => { - features.userMigrationEnabled = false; + config.FEATURE_USER_MIGRATION_ENABLED = false; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [SchoolFeature.LDAP_UNIVENTION_MIGRATION], @@ -174,9 +178,9 @@ describe(UserImportService.name, () => { }); }); - describe('when the features are disabled', () => { + describe('when the config are disabled', () => { const setup = () => { - features.userMigrationEnabled = false; + config.FEATURE_USER_MIGRATION_ENABLED = false; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [], diff --git a/apps/server/src/modules/user-import/service/user-import.service.ts b/apps/server/src/modules/user-import/service/user-import.service.ts index 4b47451f74c..db3fbeb5c70 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.ts @@ -1,22 +1,23 @@ import { LegacySchoolService } from '@modules/legacy-school'; import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; -import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { SchoolFeature } from '@shared/domain/types'; import { ImportUserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; +import { UserImportConfig } from '../user-import-config'; @Injectable() export class UserImportService { constructor( + private readonly configService: ConfigService, private readonly userImportRepo: ImportUserRepo, private readonly systemService: SystemService, private readonly userService: UserService, - @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, private readonly logger: Logger, private readonly schoolService: LegacySchoolService ) {} @@ -26,7 +27,7 @@ export class UserImportService { } public async getMigrationSystem(): Promise { - const systemId: string = this.userImportFeatures.userMigrationSystemId; + const systemId: string = this.configService.get('FEATURE_USER_MIGRATION_SYSTEM_ID'); const system: System = await this.systemService.findByIdOrFail(systemId); @@ -34,7 +35,7 @@ export class UserImportService { } public checkFeatureEnabled(school: LegacySchoolDo): void { - const enabled = this.userImportFeatures.userMigrationEnabled; + const enabled = this.configService.get('FEATURE_USER_MIGRATION_ENABLED'); const isLdapPilotSchool = school.features && school.features.includes(SchoolFeature.LDAP_UNIVENTION_MIGRATION); if (!enabled && !isLdapPilotSchool) { diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts index f9ca795e3e3..8394306979f 100644 --- a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts @@ -1,15 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; +import { ConfigService } from '@nestjs/config'; import { System } from '@modules/system'; import { SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { ImportUser, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { importUserFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; import { UserImportFetchUc } from './user-import-fetch.uc'; describe(UserImportFetchUc.name, () => { @@ -19,7 +20,13 @@ describe(UserImportFetchUc.name, () => { let schulconnexFetchImportUsersService: DeepMocked; let authorizationService: DeepMocked; let userImportService: DeepMocked; - let userImportFeatures: IUserImportFeatures; + + const config: UserImportConfig = { + FEATURE_USER_MIGRATION_ENABLED: true, + FEATURE_USER_MIGRATION_SYSTEM_ID: new ObjectId().toHexString(), + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: true, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: 0, + }; beforeAll(async () => { await setupEntities(); @@ -28,8 +35,10 @@ describe(UserImportFetchUc.name, () => { providers: [ UserImportFetchUc, { - provide: UserImportFeatures, - useValue: {}, + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof UserImportConfig) => config[key]), + }, }, { provide: SchulconnexFetchImportUsersService, @@ -50,15 +59,12 @@ describe(UserImportFetchUc.name, () => { schulconnexFetchImportUsersService = module.get(SchulconnexFetchImportUsersService); authorizationService = module.get(AuthorizationService); userImportService = module.get(UserImportService); - userImportFeatures = module.get(UserImportFeatures); }); beforeEach(() => { - Object.assign(userImportFeatures, { - userMigrationEnabled: true, - userMigrationSystemId: new ObjectId().toHexString(), - useWithUserLoginMigration: true, - }); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = new ObjectId().toHexString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; }); afterAll(async () => { @@ -74,7 +80,7 @@ describe(UserImportFetchUc.name, () => { const setup = () => { const system: SystemEntity = systemEntityFactory.buildWithId( undefined, - userImportFeatures.userMigrationSystemId + config.FEATURE_USER_MIGRATION_SYSTEM_ID ); const systemDo: System = systemFactory.build({ id: system.id }); const user: User = userFactory.buildWithId(); @@ -142,7 +148,7 @@ describe(UserImportFetchUc.name, () => { describe('when the migration feature is not enabled', () => { const setup = () => { - userImportFeatures.userMigrationEnabled = false; + config.FEATURE_USER_MIGRATION_ENABLED = false; const user: User = userFactory.buildWithId(); @@ -160,7 +166,7 @@ describe(UserImportFetchUc.name, () => { describe('when the target system id is not defined', () => { const setup = () => { - userImportFeatures.userMigrationSystemId = ''; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = ''; const user: User = userFactory.buildWithId(); diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts index 1e08f64f3b2..743e942449d 100644 --- a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts @@ -1,17 +1,18 @@ import { AuthorizationService } from '@modules/authorization'; import { System } from '@modules/system'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ImportUser, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; @Injectable() export class UserImportFetchUc { constructor( - @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, + private readonly configService: ConfigService, private readonly schulconnexFetchImportUsersService: SchulconnexFetchImportUsersService, private readonly authorizationService: AuthorizationService, private readonly userImportService: UserImportService @@ -28,7 +29,7 @@ export class UserImportFetchUc { const filteredFetchedData: ImportUser[] = await this.schulconnexFetchImportUsersService.filterAlreadyMigratedUser( fetchedData, - this.userImportFeatures.userMigrationSystemId + this.configService.get('FEATURE_USER_MIGRATION_SYSTEM_ID') ); const matchedImportUsers: ImportUser[] = await this.userImportService.matchUsers(filteredFetchedData); @@ -39,7 +40,10 @@ export class UserImportFetchUc { } private checkMigrationEnabled(userId: EntityId): void { - if (!this.userImportFeatures.userMigrationEnabled || !this.userImportFeatures.userMigrationSystemId) { + if ( + !this.configService.get('FEATURE_USER_MIGRATION_ENABLED') || + !this.configService.get('FEATURE_USER_MIGRATION_SYSTEM_ID') + ) { throw new UserMigrationIsNotEnabledLoggableException(userId); } } 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 147dba2887e..5b22368f4f3 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 @@ -9,6 +9,7 @@ import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -30,9 +31,9 @@ import { userLoginMigrationDOFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable } from '../loggable'; import { UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -56,7 +57,12 @@ describe('[ImportUserModule]', () => { let userMigrationService: DeepMocked; let logger: DeepMocked; - let userImportFeatures: IUserImportFeatures; + const config: UserImportConfig = { + FEATURE_USER_MIGRATION_ENABLED: true, + FEATURE_USER_MIGRATION_SYSTEM_ID: new ObjectId().toHexString(), + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: false, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: 80000, + }; beforeAll(async () => { await setupEntities(); @@ -64,6 +70,12 @@ describe('[ImportUserModule]', () => { module = await Test.createTestingModule({ providers: [ UserImportUc, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof UserImportConfig) => config[key]), + }, + }, { provide: AccountService, useValue: createMock(), @@ -100,10 +112,6 @@ describe('[ImportUserModule]', () => { provide: UserLoginMigrationService, useValue: createMock(), }, - { - provide: UserImportFeatures, - useValue: {}, - }, { provide: UserMigrationService, useValue: createMock(), @@ -124,18 +132,15 @@ describe('[ImportUserModule]', () => { userService = module.get(UserService); authorizationService = module.get(AuthorizationService); userImportService = module.get(UserImportService); - userImportFeatures = module.get(UserImportFeatures); userLoginMigrationService = module.get(UserLoginMigrationService); userMigrationService = module.get(UserMigrationService); logger = module.get(Logger); }); beforeEach(() => { - Object.assign(userImportFeatures, { - userMigrationEnabled: true, - userMigrationSystemId: new ObjectId().toHexString(), - useWithUserLoginMigration: false, - }); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = new ObjectId().toHexString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }); afterAll(async () => { @@ -658,7 +663,7 @@ describe('[ImportUserModule]', () => { userService.findByExternalId.mockResolvedValueOnce(null); schoolService.getSchoolById.mockResolvedValueOnce(school); importUserRepo.findImportUsers.mockResolvedValueOnce([[importUser, importUserWithoutUser], 2]); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, @@ -726,7 +731,7 @@ describe('[ImportUserModule]', () => { userDoFactory.buildWithId({ id: user.id, externalId: user.externalId }) ); importUserRepo.findImportUsers.mockResolvedValueOnce([[importUser, importUserWithoutUser], 2]); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, @@ -820,7 +825,7 @@ describe('[ImportUserModule]', () => { schoolServiceSaveSpy = schoolService.save.mockReturnValueOnce(Promise.resolve(createMockSchoolDo(school))); schoolServiceSpy = schoolService.getSchoolById.mockResolvedValue(createMockSchoolDo(school)); systemRepoSpy = systemService.findById.mockReturnValueOnce(Promise.resolve(systemDo)); - userImportFeatures.userMigrationSystemId = system.id; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system.id; dateSpy = jest.spyOn(global, 'Date').mockReturnValue(currentDate as unknown as string); }); @@ -932,7 +937,7 @@ describe('[ImportUserModule]', () => { targetSystemId, }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); @@ -982,7 +987,7 @@ describe('[ImportUserModule]', () => { closedAt: new Date(), }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); @@ -1012,7 +1017,7 @@ describe('[ImportUserModule]', () => { systems: [targetSystemId], }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); @@ -1045,7 +1050,7 @@ describe('[ImportUserModule]', () => { targetSystemId, }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); @@ -1142,7 +1147,7 @@ describe('[ImportUserModule]', () => { userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, @@ -1171,7 +1176,7 @@ describe('[ImportUserModule]', () => { userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index be63375ad4e..6a8547fc4a3 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -5,7 +5,8 @@ import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; -import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; @@ -14,7 +15,6 @@ import { IFindOptions, Permission } from '@shared/domain/interface'; import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; import { ImportUserRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { MigrationMayBeCompleted, MigrationMayNotBeCompleted, @@ -26,6 +26,7 @@ import { } from '../loggable'; import { UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -40,6 +41,7 @@ export type UserImportPermissions = @Injectable() export class UserImportUc { constructor( + private readonly configService: ConfigService, private readonly accountService: AccountService, private readonly importUserRepo: ImportUserRepo, private readonly authorizationService: AuthorizationService, @@ -49,7 +51,6 @@ export class UserImportUc { private readonly userService: UserService, private readonly logger: Logger, private readonly userImportService: UserImportService, - @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, private readonly userLoginMigrationService: UserLoginMigrationService, private readonly userMigrationService: UserMigrationService ) { @@ -238,9 +239,11 @@ export class UserImportUc { } public async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { - const { useWithUserLoginMigration } = this.userImportFeatures; + const FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = this.configService.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ); - if (useWithUserLoginMigration) { + if (FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { useCentralLdap = false; } @@ -248,11 +251,11 @@ export class UserImportUc { const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); this.userImportService.checkFeatureEnabled(school); - if (useCentralLdap || useWithUserLoginMigration) { + if (useCentralLdap || FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { this.checkSchoolNumber(school); } this.checkSchoolNotInMigration(school); - if (useWithUserLoginMigration) { + if (FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { await this.checkSchoolMigrated(currentUser.school.id, school); await this.checkMigrationActive(currentUser.school.id); } else { @@ -261,7 +264,7 @@ export class UserImportUc { this.logger.notice(new SchoolInUserMigrationStartLoggable(currentUserId, school.name, useCentralLdap)); - if (!useWithUserLoginMigration) { + if (!FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { school.externalId = school.officialSchoolNumber; } @@ -316,7 +319,9 @@ export class UserImportUc { school.inMaintenanceSince = undefined; - const isMigrationRestartable: boolean = this.userImportFeatures.useWithUserLoginMigration; + const isMigrationRestartable: boolean = this.configService.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ); if (isMigrationRestartable) { school.inUserMigration = undefined; } @@ -343,9 +348,11 @@ export class UserImportUc { } private async updateUserAndAccount(importUser: ImportUser, school: LegacySchoolDo): Promise { - const { useWithUserLoginMigration } = this.userImportFeatures; + const FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = this.configService.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ); - if (useWithUserLoginMigration) { + if (FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { await this.updateUserAndAccountWithUserLoginMigration(importUser); } else { await this.updateUserAndAccountWithLdap(importUser, school); diff --git a/apps/server/src/modules/user-import/user-import-config.module.ts b/apps/server/src/modules/user-import/user-import-config.module.ts deleted file mode 100644 index 34ba1e5f994..00000000000 --- a/apps/server/src/modules/user-import/user-import-config.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserImportConfiguration, UserImportFeatures } from './config'; - -@Module({ - providers: [ - { - provide: UserImportFeatures, - useValue: UserImportConfiguration.userImportFeatures, - }, - ], - exports: [UserImportFeatures], -}) -export class UserImportConfigModule {} diff --git a/apps/server/src/modules/user-import/user-import-config.ts b/apps/server/src/modules/user-import/user-import-config.ts index a7145a56d1b..ce005c20ce5 100644 --- a/apps/server/src/modules/user-import/user-import-config.ts +++ b/apps/server/src/modules/user-import/user-import-config.ts @@ -1,3 +1,6 @@ export interface UserImportConfig { + FEATURE_USER_MIGRATION_ENABLED: boolean; + FEATURE_USER_MIGRATION_SYSTEM_ID: string; + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: boolean; IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: number; } diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index b73db2aebda..b69b80b9fd1 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -13,7 +13,6 @@ import { LoggerModule } from '@src/core/logger'; import { ImportUserController } from './controller/import-user.controller'; import { SchulconnexFetchImportUsersService, UserImportService } from './service'; import { UserImportFetchUc, UserImportUc } from './uc'; -import { UserImportConfigModule } from './user-import-config.module'; @Module({ imports: [ @@ -21,7 +20,6 @@ import { UserImportConfigModule } from './user-import-config.module'; AccountModule, LegacySchoolModule, AuthorizationModule, - UserImportConfigModule, HttpModule, UserModule, OauthModule, diff --git a/apps/server/src/modules/video-conference/bbb/bbb-config.ts b/apps/server/src/modules/video-conference/bbb/bbb-config.ts new file mode 100644 index 00000000000..61488050134 --- /dev/null +++ b/apps/server/src/modules/video-conference/bbb/bbb-config.ts @@ -0,0 +1,5 @@ +export interface BbbConfig { + VIDEOCONFERENCE_HOST: string; + VIDEOCONFERENCE_SALT: string; + VIDEOCONFERENCE_DEFAULT_PRESENTATION: string; +} diff --git a/apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts b/apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts deleted file mode 100644 index c250bebb929..00000000000 --- a/apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const BbbSettings = Symbol('BbbSettings'); - -export interface IBbbSettings { - host: string; - salt: string; - presentationUrl: string; -} diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts index 1731d10ff8e..9c643f80698 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ConverterUtil } from '@shared/common'; import { axiosResponseFactory } from '@shared/testing'; @@ -9,7 +10,8 @@ import { AxiosResponse } from 'axios'; import crypto, { Hash } from 'crypto'; import { of } from 'rxjs'; import { URLSearchParams } from 'url'; -import { BbbSettings, IBbbSettings } from './bbb-settings.interface'; +import { VideoConferenceConfig } from '../video-conference-config'; +import { BbbConfig } from './bbb-config'; import { BBBService } from './bbb.service'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig, BBBRole, GuestPolicy } from './request'; import { BBBBaseResponse, BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse, BBBStatus } from './response'; @@ -106,23 +108,20 @@ class BBBServiceTest extends BBBService { } } -describe('BBB Service', () => { +describe(BBBService.name, () => { let module: TestingModule; let service: BBBServiceTest; let httpService: DeepMocked; let converterUtil: DeepMocked; + let configService: DeepMocked>; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ BBBServiceTest, { - provide: BbbSettings, - useValue: createMock({ - host: 'https://bbb.de', - salt: 'salt12345', - presentationUrl: '', - }), + provide: ConfigService, + useValue: createMock>(), }, { provide: HttpService, @@ -137,12 +136,17 @@ describe('BBB Service', () => { service = module.get(BBBServiceTest); httpService = module.get(HttpService); converterUtil = module.get(ConverterUtil); + configService = module.get(ConfigService); }); afterAll(async () => { await module.close(); }); + beforeEach(() => { + configService.get.mockReturnValue('https://mocked'); + }); + describe('create', () => { describe('when valid parameter passed and the BBB response well', () => { const setup = () => { @@ -200,13 +204,10 @@ describe('BBB Service', () => { }); it('should return a xml configuration with provided presentation url', () => { - // Arrange const presentationUrl = 'https://s3.hidrive.strato.com/cloud-instances/bbb/presentation.pdf'; - // Act const result = service.getBbbRequestConfig(presentationUrl); - // Assert expect(result).toBe( "" ); @@ -377,13 +378,10 @@ describe('BBB Service', () => { }); it('toParams: should return params based on bbb configs', () => { - // Arrange const createConfig: BBBCreateConfig = createBBBCreateConfig(); - // Act const params: URLSearchParams = service.superToParams(createConfig); - // Assert expect(params.get('name')).toEqual(createConfig.name); expect(params.get('meetingID')).toEqual(createConfig.meetingID); expect(params.get('logoutURL')).toEqual(createConfig.logoutURL); @@ -408,24 +406,19 @@ describe('BBB Service', () => { const sha = crypto.createHash('sha1'); const expectedChecksum: string = sha.update(callName + queryString + service.getSalt()).digest('hex'); - // Act const checksum: string = service.superGenerateChecksum(callName, urlSearchParams); - // Assert expect(checksum).toEqual(expectedChecksum); expect(createHashMock).toBeCalledWith('sha1'); }); it('getUrl: should return composed url', () => { - // Arrange const createConfig = createBBBCreateConfig(); const callName = 'create'; const params: URLSearchParams = service.superToParams(createConfig); - // Act const url: string = service.superGetUrl(callName, params); - // Assert expect(url.toString()).toContain(`${service.getBaseUrl()}/bigbluebutton/api/${callName}`); expect(url.includes('checksum')).toBeTruthy(); }); diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.ts index 2fcc3db9981..b40e47053b4 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.ts @@ -1,33 +1,35 @@ import { HttpService } from '@nestjs/axios'; -import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ConverterUtil } from '@shared/common/utils'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; import crypto from 'crypto'; import { firstValueFrom, Observable } from 'rxjs'; import { URL, URLSearchParams } from 'url'; -import { BbbSettings, IBbbSettings } from './bbb-settings.interface'; +import { VideoConferenceConfig } from '../video-conference-config'; +import { BbbConfig } from './bbb-config'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig } from './request'; import { BBBBaseResponse, BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse, BBBStatus } from './response'; @Injectable() export class BBBService { constructor( - @Inject(BbbSettings) private readonly bbbSettings: IBbbSettings, + private readonly configService: ConfigService, private readonly httpService: HttpService, private readonly converterUtil: ConverterUtil ) {} protected get baseUrl(): string { - return this.bbbSettings.host; + return this.configService.get('VIDEOCONFERENCE_HOST'); } protected get salt(): string { - return this.bbbSettings.salt; + return this.configService.get('VIDEOCONFERENCE_SALT'); } protected get presentationUrl(): string { - return this.bbbSettings.presentationUrl; + return this.configService.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION'); } /** diff --git a/apps/server/src/modules/video-conference/bbb/index.ts b/apps/server/src/modules/video-conference/bbb/index.ts index 66f3703a950..cc35a1e2d07 100644 --- a/apps/server/src/modules/video-conference/bbb/index.ts +++ b/apps/server/src/modules/video-conference/bbb/index.ts @@ -1,5 +1,5 @@ -export * from './bbb-settings.interface'; export * from './request'; export * from './builder'; export * from './response'; export * from './bbb.service'; +export { BbbConfig } from './bbb-config'; diff --git a/apps/server/src/modules/video-conference/index.ts b/apps/server/src/modules/video-conference/index.ts index 16071ac6627..7564a72653f 100644 --- a/apps/server/src/modules/video-conference/index.ts +++ b/apps/server/src/modules/video-conference/index.ts @@ -1,3 +1,2 @@ export { VideoConferenceModule } from './video-conference.module'; -export { IVideoConferenceSettings } from './interface'; -export { default as VideoConferenceConfiguration } from './video-conference-config'; +export { VideoConferenceConfig } from './video-conference-config'; diff --git a/apps/server/src/modules/video-conference/interface/index.ts b/apps/server/src/modules/video-conference/interface/index.ts index 5df41ce67c3..6c797da8dc5 100644 --- a/apps/server/src/modules/video-conference/interface/index.ts +++ b/apps/server/src/modules/video-conference/interface/index.ts @@ -1,2 +1 @@ -export * from './video-conference-settings.interface'; export * from './video-conference-options.interface'; diff --git a/apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts b/apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts deleted file mode 100644 index a1d07fb516a..00000000000 --- a/apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IBbbSettings } from '../bbb'; - -export const VideoConferenceSettings = Symbol('VideoConferenceSettings'); - -export interface IVideoConferenceSettings { - enabled: boolean; - hostUrl: string; - bbb: IBbbSettings; -} 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 d0daef948aa..3bc50b9f20f 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 @@ -1,11 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { CalendarEventDto, CalendarService } from '@infra/calendar'; +import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { CourseService } from '@modules/learnroom/service'; import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO, VideoConferenceDO } from '@shared/domain/domainobject'; import { Course, TeamUserEntity } from '@shared/domain/entity'; @@ -16,14 +18,14 @@ import { courseFactory, roleFactory, setupEntities, userDoFactory, userFactory } import { teamFactory } from '@shared/testing/factory/team.factory'; import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; -import { ObjectId } from '@mikro-orm/mongodb'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error'; -import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; +import { VideoConferenceOptions } from '../interface'; import { ScopeInfo, ScopeRef, VideoConferenceState } from '../uc/dto'; +import { VideoConferenceConfig } from '../video-conference-config'; import { VideoConferenceService } from './video-conference.service'; -describe('VideoConferenceService', () => { +describe(VideoConferenceService.name, () => { let service: DeepMocked; let courseService: DeepMocked; let calendarService: DeepMocked; @@ -32,17 +34,15 @@ describe('VideoConferenceService', () => { let teamsRepo: DeepMocked; let userService: DeepMocked; let videoConferenceRepo: DeepMocked; - let videoConferenceSettings: DeepMocked; + let configService: DeepMocked>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ VideoConferenceService, { - provide: VideoConferenceSettings, - useValue: createMock({ - hostUrl: 'https://api.example.com', - }), + provide: ConfigService, + useValue: createMock>(), }, { provide: CourseService, @@ -83,13 +83,15 @@ describe('VideoConferenceService', () => { teamsRepo = module.get(TeamsRepo); userService = module.get(UserService); videoConferenceRepo = module.get(VideoConferenceRepo); - videoConferenceSettings = module.get(VideoConferenceSettings); + configService = module.get(ConfigService); await setupEntities(); }); describe('canGuestJoin', () => { const setup = (isGuest: boolean, state: VideoConferenceState, waitingRoomEnabled: boolean) => { + configService.get.mockReturnValue('https://api.example.com'); + return { isGuest, state, @@ -139,6 +141,7 @@ describe('VideoConferenceService', () => { const userId = user.id as EntityId; const scopeId = new ObjectId().toHexString(); + configService.get.mockReturnValue('https://api.example.com'); userService.findById.mockResolvedValue(user); return { @@ -492,7 +495,8 @@ describe('VideoConferenceService', () => { describe('when video conference feature is globally disabled', () => { it('should throw a ForbiddenException', async () => { const { schoolId } = setup(false); - videoConferenceSettings.enabled = false; + + configService.get.mockReturnValue(false); const func = () => service.throwOnFeaturesDisabled(schoolId); @@ -540,6 +544,8 @@ describe('VideoConferenceService', () => { const conferenceScope: VideoConferenceScope = VideoConferenceScope.COURSE; const scopeId = new ObjectId().toHexString(); + configService.get.mockReturnValue('https://api.example.com'); + return { userId, conferenceScope, @@ -605,6 +611,8 @@ describe('VideoConferenceService', () => { .withRoleAndUserId(roleFactory.build({ name: RoleName.EXPERT }), new ObjectId().toHexString()) .build(); + configService.get.mockReturnValue('https://api.example.com'); + return { user, userId, 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 13604e32b12..f910e9e215b 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 @@ -3,7 +3,8 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/auth import { CourseService } from '@modules/learnroom'; import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; -import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { RoleReference, UserDO, VideoConferenceDO, VideoConferenceOptionsDO } from '@shared/domain/domainobject'; import { Course, TeamEntity, TeamUserEntity, User } from '@shared/domain/entity'; import { Permission, RoleName, VideoConferenceScope } from '@shared/domain/interface'; @@ -11,13 +12,14 @@ import { EntityId, SchoolFeature } from '@shared/domain/types'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error'; -import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; +import { VideoConferenceOptions } from '../interface'; import { ScopeInfo, VideoConferenceState } from '../uc/dto'; +import { VideoConferenceConfig } from '../video-conference-config'; @Injectable() export class VideoConferenceService { constructor( - @Inject(VideoConferenceSettings) private readonly vcSettings: IVideoConferenceSettings, + private readonly configService: ConfigService, private readonly courseService: CourseService, private readonly calendarService: CalendarService, private readonly authorizationService: AuthorizationService, @@ -28,21 +30,25 @@ export class VideoConferenceService { ) {} get hostUrl(): string { - return this.vcSettings.hostUrl; + return this.configService.get('HOST'); } get isVideoConferenceFeatureEnabled(): boolean { - return this.vcSettings.enabled; + return this.configService.get('FEATURE_VIDEOCONFERENCE_ENABLED'); } - canGuestJoin(isGuest: boolean, state: VideoConferenceState, waitingRoomEnabled: boolean): boolean { + public canGuestJoin(isGuest: boolean, state: VideoConferenceState, waitingRoomEnabled: boolean): boolean { if ((isGuest && state === VideoConferenceState.NOT_STARTED) || (isGuest && !waitingRoomEnabled)) { return false; } return true; } - async hasExpertRole(userId: EntityId, conferenceScope: VideoConferenceScope, scopeId: string): Promise { + public async hasExpertRole( + userId: EntityId, + conferenceScope: VideoConferenceScope, + scopeId: string + ): Promise { let isExpert = false; switch (conferenceScope) { case VideoConferenceScope.COURSE: { @@ -136,7 +142,7 @@ export class VideoConferenceService { throw new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION); } - async throwOnFeaturesDisabled(schoolId: EntityId): Promise { + public async throwOnFeaturesDisabled(schoolId: EntityId): Promise { if (!this.isVideoConferenceFeatureEnabled) { throw new ForbiddenException( ErrorStatus.SCHOOL_FEATURE_DISABLED, @@ -150,11 +156,11 @@ export class VideoConferenceService { } } - sanitizeString(text: string) { + public sanitizeString(text: string) { return text.replace(/[^\dA-Za-zÀ-ÖØ-öø-ÿ.\-=_`´ ]/g, ''); } - async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise { + public async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise { switch (scope) { case VideoConferenceScope.COURSE: { const course: Course = await this.courseService.findById(scopeId); @@ -181,7 +187,7 @@ export class VideoConferenceService { } } - async getUserRoleAndGuestStatusByUserIdForBbb( + public async getUserRoleAndGuestStatusByUserIdForBbb( userId: string, scopeId: EntityId, scope: VideoConferenceScope @@ -195,7 +201,7 @@ export class VideoConferenceService { return { role, isGuest: isBbbGuest }; } - async findVideoConferenceByScopeIdAndScope( + public async findVideoConferenceByScopeIdAndScope( scopeId: EntityId, scope: VideoConferenceScope ): Promise { @@ -204,7 +210,7 @@ export class VideoConferenceService { return videoConference; } - async createOrUpdateVideoConferenceForScopeWithOptions( + public async createOrUpdateVideoConferenceForScopeWithOptions( scopeId: EntityId, scope: VideoConferenceScope, options: VideoConferenceOptions diff --git a/apps/server/src/modules/video-conference/video-conference-config.ts b/apps/server/src/modules/video-conference/video-conference-config.ts index 7e65d0e7155..0753cd811eb 100644 --- a/apps/server/src/modules/video-conference/video-conference-config.ts +++ b/apps/server/src/modules/video-conference/video-conference-config.ts @@ -1,17 +1,4 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { IBbbSettings } from './bbb'; -import { IVideoConferenceSettings } from './interface'; - -export default class VideoConferenceConfiguration { - static bbb: IBbbSettings = { - host: Configuration.get('VIDEOCONFERENCE_HOST') as string, - salt: Configuration.get('VIDEOCONFERENCE_SALT') as string, - presentationUrl: Configuration.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION') as string, - }; - - static videoConference: IVideoConferenceSettings = { - enabled: Configuration.get('FEATURE_VIDEOCONFERENCE_ENABLED') as boolean, - hostUrl: Configuration.get('HOST') as string, - bbb: VideoConferenceConfiguration.bbb, - }; +export interface VideoConferenceConfig { + HOST: string; + FEATURE_VIDEOCONFERENCE_ENABLED: boolean; } 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 db0a539e5fc..0b7bf02841a 100644 --- a/apps/server/src/modules/video-conference/video-conference.module.ts +++ b/apps/server/src/modules/video-conference/video-conference.module.ts @@ -10,12 +10,10 @@ import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; import { LoggerModule } from '@src/core/logger'; import { LearnroomModule } from '../learnroom'; -import { BBBService, BbbSettings } from './bbb'; +import { BBBService } from './bbb'; import { VideoConferenceDeprecatedController } from './controller'; -import { VideoConferenceSettings } from './interface'; import { VideoConferenceService } from './service'; import { VideoConferenceDeprecatedUc } from './uc'; -import VideoConferenceConfiguration from './video-conference-config'; @Module({ imports: [ @@ -30,14 +28,6 @@ import VideoConferenceConfiguration from './video-conference-config'; UserModule, ], providers: [ - { - provide: VideoConferenceSettings, - useValue: VideoConferenceConfiguration.videoConference, - }, - { - provide: BbbSettings, - useValue: VideoConferenceConfiguration.bbb, - }, BBBService, VideoConferenceRepo, // TODO: N21-1010 clean up video conferences - remove repos From 324b9e65b850368d97b0a35d5b95cb70b0cd1742 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport <106819770+hoeppner-dataport@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:21:02 +0200 Subject: [PATCH 29/35] BC-6854 - basic load tests (#5099) Implementation of basic load test capabilities for websocket functionality on the board. * improve performance of findCards by adding new function getBoardAuthorizables * track number of action calls in separate metric * implementation of a load testing shell script * added option to pass target and scenario as parameters to the shell script * readme.md for board related load testing --------- Co-authored-by: Thomas Feldtkeller --- .gitignore | 2 + .../gateway/board-collaboration.gateway.ts | 14 ++ .../src/modules/board/loadtest/readme.md | 76 ++++++++++ .../src/modules/board/loadtest/runScenario.sh | 139 ++++++++++++++++++ .../loadtest/scenarios/30users_5minutes.yml | 75 ++++++++++ .../board/loadtest/scenarios/3users.yml | 57 +++++++ .../scenarios/6createCard_6UpdateTitle.yml | 70 +++++++++ .../modules/board/metrics/metrics.service.ts | 52 ++++++- .../board-node-authorizable.service.ts | 49 +++++- .../board/service/board-node.service.ts | 6 + .../src/modules/board/uc/card.uc.spec.ts | 23 ++- apps/server/src/modules/board/uc/card.uc.ts | 17 +-- package-lock.json | 18 ++- package.json | 2 +- 14 files changed, 574 insertions(+), 26 deletions(-) create mode 100644 apps/server/src/modules/board/loadtest/readme.md create mode 100644 apps/server/src/modules/board/loadtest/runScenario.sh create mode 100644 apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml create mode 100644 apps/server/src/modules/board/loadtest/scenarios/3users.yml create mode 100644 apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml diff --git a/.gitignore b/.gitignore index 130655cf16d..a045507c02b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ build /coverage /.nyc_output /.idea/ +/apps/server/src/modules/board/loadtest/**/*.html +/apps/server/src/modules/board/loadtest/artilleryreport.json diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index a04045d0b53..c9a6cc7163b 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -61,6 +61,10 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { trackExecutionTime(methodName: string, executionTimeMs: number) { if (this.metricsService) { this.metricsService.setExecutionTime(methodName, executionTimeMs); + this.metricsService.incrementActionCount(methodName); + this.metricsService.incrementActionGauge(methodName); + this.metricsService.incrementActionCount('all'); + this.metricsService.incrementActionGauge('all'); } } @@ -128,6 +132,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-card-height-request') + @TrackExecutionTime() @UseRequestContext() async updateCardHeight(socket: Socket, data: UpdateCardHeightMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-height' }); @@ -142,6 +147,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-card-request') + @TrackExecutionTime() @UseRequestContext() async deleteCard(socket: Socket, data: DeleteCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-card' }); @@ -178,6 +184,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-column-request') + @TrackExecutionTime() @UseRequestContext() async createColumn(socket: Socket, data: CreateColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-column' }); @@ -219,6 +226,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-card-request') + @TrackExecutionTime() @UseRequestContext() async moveCard(socket: Socket, data: MoveCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-card' }); @@ -233,6 +241,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-column-request') + @TrackExecutionTime() @UseRequestContext() async moveColumn(socket: Socket, data: MoveColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-column' }); @@ -267,6 +276,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-board-visibility-request') + @TrackExecutionTime() @UseRequestContext() async updateBoardVisibility(socket: Socket, data: UpdateBoardVisibilityMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-visibility' }); @@ -281,6 +291,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-column-request') + @TrackExecutionTime() @UseRequestContext() async deleteColumn(socket: Socket, data: DeleteColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-column' }); @@ -312,6 +323,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-element-request') + @TrackExecutionTime() @UseRequestContext() async createElement(socket: Socket, data: CreateContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-element' }); @@ -346,6 +358,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-element-request') + @TrackExecutionTime() @UseRequestContext() async deleteElement(socket: Socket, data: DeleteContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-element' }); @@ -361,6 +374,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-element-request') + @TrackExecutionTime() @UseRequestContext() async moveElement(socket: Socket, data: MoveContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-element' }); diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md new file mode 100644 index 00000000000..24cbd18605c --- /dev/null +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -0,0 +1,76 @@ +# Loadtesting the boards + +The socket.io documentation suggests to use the tool artillery in order to load test a socket-io tool like our board-collaboration service. + +For defining scenarios you need to use/create Yaml-files that define which operations with which parameters need to be executed in which order. + +Some sceneraios were already prepared and are stored in the subfolder scenarios. + +## install artillery + +To run artillery from your local environment you need to install it first including an adapter that supports socketio-v3-websocket communication: + +```sh +npm install -g artillery artillery-engine-socketio-v3 +``` + +## manual execution + +To execute a scenario you can run artillery from the shell / commandline...: + +Using the `--variables` parameter it is possible to define several variables and there values that can be used in the scenerio-yaml-file: + +- **target**: defines the base url for all requests (REST and WebSocket) + e.g. `https://main.dbc.dbildungscloud.dev` +- **token**: a valid JWT for the targeted system +- **board_id**: id of an existing board the tests should be executed on + +```bash +npx artillery run --variables "{'target': 'https://main.dbc.dbildungscloud.dev', 'token': 'eJ....', 'board_id': '668d0e03bf3689d12e1e86fb' }" './scenarios/3users.yml' --output artilleryreport.json +``` + +On Windows Powershell, the variables value needs to be wrapped in singlequotes, and inside the json you need to use backslash-escaped doublequotes: + +```powershell +npx artillery run --variables '{\"target\": \"https://main.dbc.dbildungscloud.dev\", \"token\": \"eJ....\", \"board_id\": \"668d0e03bf3689d12e1e86fb\" }' './scenarios/3users.yml' --output artilleryreport.json +``` + +## visualizing the recorded results + +It is possible to generate a HTML-report based on the recorded data. + +```powershell +npx artillery report --output=$board_title.html artilleryreport.json +``` + +## automatic execution + +You can run one of the existing scenarios by executing: + +```bash +bash runScenario.sh +``` + +This will: + +1. let you choose from scenario-files +2. create a fresh JWT-webtoken +3. create a fresh board (in one of the courses) the user has access to +4. name the board by a combination of datetime and the scenario name. +5. output a link to the generated board (in order open and see the test live) +6. start the execution of the scenario against this newly created board +7. generate a html report in the end + +You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those. Here is an example: + +```bash +bash runScenario.sh https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev 3users +``` + +## password + +By typeing `export CARL_CORD_PASSWORD=realpassword` the script will not ask you anymore for the password to create a token. + +## Todos + +- [ ] enable optional parameter course_id diff --git a/apps/server/src/modules/board/loadtest/runScenario.sh b/apps/server/src/modules/board/loadtest/runScenario.sh new file mode 100644 index 00000000000..0187f1c579f --- /dev/null +++ b/apps/server/src/modules/board/loadtest/runScenario.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +function select_target() { + declare -a targets=("https://main.nbc.dbildungscloud.dev" "https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev") + echo "Please select the target for the test:" >&2 + select target in "${targets[@]}"; do + if [[ -n $target ]]; then + break + else + echo "Invalid selection. Please try again." >&2 + fi + done +} + +function select_scenario() { + # list files in the scenarios directory + scenarios_dir="./scenarios" + declare -a scenario_files=($(ls $scenarios_dir)) + + echo "Please select a scenario file for the test:" >&2 + select scenario_file in "${scenario_files[@]}"; do + if [[ -n $scenario_file ]]; then + echo "You have selected: $scenario_file" >&2 + break + else + echo "Invalid selection. Please try again." >&2 + fi + done + + scenario_name="${scenario_file%.*}" +} + +function get_credentials() { + if [ -z "$CARL_CORD_PASSWORD" ]; then + echo "Password for Carl Cord is unknown. Provide it as an enviroment variable (CARL_CORD_PASSWORD) or enter it:" + read CARL_CORD_PASSWORD + export CARL_CORD_PASSWORD + fi +} + +function get_token() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/authentication/local" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d "{ + \"username\": \"lehrer@schul-cloud.org\", + \"password\": \"$CARL_CORD_PASSWORD\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get token. Please check your credentials and target URL." >&2 + exit 1 + fi + + token=$(echo $response | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p') +} + +function get_course_id() { + response=$(curl -s -f -X 'GET' \ + "$target/api/v3/courses" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $token") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get course list. Please check your credentials and target URL." >&2 + exit 1 + fi + + course_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') +} + +function create_board_title() { + current_date=$(date +%Y-%m-%d_%H:%M) + board_title="${current_date}_$1" +} + +function create_board() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/boards" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $token" \ + -d "{ + \"title\": \"$board_title\", + \"parentId\": \"$course_id\", + \"parentType\": \"course\", + \"layout\": \"columns\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to create a board." >&2 + exit 1 + fi + + board_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' ) +} + +if [ -z "$1" ]; then + select_target +else + target=$1 +fi +echo " " +echo "target: $target" + + +if [ -z "$2" ]; then + select_scenario + echo "scenario_name: $scenario_name" +else + scenario_name="$2" + scenario_name=${scenario_name//.yml/} +fi +echo "scenario_name: $scenario_name" + +get_credentials + +get_token +echo "token: ${token:0:50}..." +echo " " + +get_course_id +echo "course_id: $course_id" +echo " " + +create_board_title $scenario_name +echo "board_title: $board_title" + +create_board +echo "board_id $board_id" + +echo "board: $target/rooms/$board_id/board" +echo " " +echo "Running artillery test..." + +npx artillery run --variables "{\"target\": \"$target\", \"token\": \"$token\", \"board_id\": \"$board_id\" }" "./scenarios/$scenario_name.yml" --output artilleryreport.json + +npx artillery report --output=$board_title.html artilleryreport.json diff --git a/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml new file mode 100644 index 00000000000..567cbaf703a --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml @@ -0,0 +1,75 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 300 + arrivalRate: 10 + maxVusers: 30 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'fetch-board-request' + data: + boardId: '{{ board_id }}' + + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 20 diff --git a/apps/server/src/modules/board/loadtest/scenarios/3users.yml b/apps/server/src/modules/board/loadtest/scenarios/3users.yml new file mode 100644 index 00000000000..4fbeef037c8 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/3users.yml @@ -0,0 +1,57 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 1 + arrivalRate: 3 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - log: '{{ target }}' + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'One {{ cardId}}' + + - think: 2 diff --git a/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml new file mode 100644 index 00000000000..ad7e993f829 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml @@ -0,0 +1,70 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 2 + arrivalRate: 50 + +scenarios: + - name: create card + engine: socketio-v3 + socketio: + extraHeaders: + Cookie: 'jwt={{ token }}' + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 2 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 6 diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index 8019220b40e..b2a54d5ce75 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { UserService } from '@src/modules/user'; -import { Gauge, Summary, register } from 'prom-client'; +import { Gauge, Summary, register, Counter } from 'prom-client'; type ClientId = string; type Role = 'owner' | 'editor' | 'viewer'; @@ -19,6 +19,10 @@ export class MetricsService { private executionTimesSummary: Map> = new Map(); + private actionCounters: Map> = new Map(); + + private actionGauges: Map> = new Map(); + constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ name: 'sc_boards_rooms', @@ -88,13 +92,55 @@ export class MetricsService { summary = new Summary({ name: `sc_boards_execution_time_${actionName}`, help: 'Average execution time of a specific action in milliseconds', - maxAgeSeconds: 60, + maxAgeSeconds: 600, ageBuckets: 5, - percentiles: [0.01, 0.1, 0.9, 0.99], + percentiles: [0.01, 0.1, 0.5, 0.9, 0.99], + pruneAgedBuckets: true, }); this.executionTimesSummary.set(actionName, summary); register.registerMetric(summary); } + console.log(actionName, `executionTime: ${value.toFixed(3)} ms`); summary.observe(value); } + + public incrementActionCount(actionName: string): void { + let counter = this.actionCounters.get(actionName); + + if (!counter) { + counter = new Counter({ + name: `sc_boards_count_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionCounters.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } + + public incrementActionGauge(actionName: string): void { + let counter = this.actionGauges.get(actionName); + + if (!counter) { + counter = new Gauge({ + name: `sc_boards_count2_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionGauges.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } } diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.ts index bc9a846311f..b813761163b 100644 --- a/apps/server/src/modules/board/service/board-node-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { type EntityId } from '@shared/domain/types'; import { type AuthorizationLoaderService } from '@modules/authorization'; -import { AnyBoardNode, BoardNodeAuthorizable } from '../domain'; +import { AnyBoardNode, BoardNodeAuthorizable, UserWithBoardRoles } from '../domain'; import { BoardNodeRepo } from '../repo'; import { BoardContextService } from './internal/board-context.service'; import { BoardNodeService } from './board-node.service'; @@ -40,4 +40,51 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService return boardNodeAuthorizable; } + + async getBoardAuthorizables(boardNodes: AnyBoardNode[]): Promise { + const rootIds = boardNodes.map((node) => node.rootId); + const parentIds = boardNodes.map((node) => node.parentId).filter((defined) => defined) as EntityId[]; + const boardNodeMap = await this.getBoardNodeMap([...rootIds, ...parentIds]); + const promises = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + return this.boardContextService.getUsersWithBoardRoles(rootNode).then((users) => { + return { id: boardNode.id, users }; + }); + }); + + const results = await Promise.all(promises); + const usersMap = results.reduce((acc, { id, users }) => { + acc[id] = users; + return acc; + }, {} as Record); + + const boardNodeAuthorizables = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + const parentNode = boardNode.parentId ? boardNodeMap[boardNode.parentId] : undefined; + const users = usersMap[boardNode.id]; + const boardNodeAuthorizable = new BoardNodeAuthorizable({ + users, + id: boardNode.id, + boardNode, + rootNode, + parentNode, + }); + return boardNodeAuthorizable; + }); + + return boardNodeAuthorizables; + } + + private async getBoardNodeMap(ids: EntityId[]): Promise> { + const idsUnique = Array.from(new Set(ids)); + const boardNodes = await this.boardNodeService.findByIds(idsUnique, 1); + const nodesMap: Record = boardNodes.reduce( + (map: Record, boardNode) => { + map[boardNode.id] = boardNode; + return map; + }, + {} as Record + ); + return nodesMap; + } } diff --git a/apps/server/src/modules/board/service/board-node.service.ts b/apps/server/src/modules/board/service/board-node.service.ts index bd8092a2e78..0ac96b627b1 100644 --- a/apps/server/src/modules/board/service/board-node.service.ts +++ b/apps/server/src/modules/board/service/board-node.service.ts @@ -76,6 +76,12 @@ export class BoardNodeService { return boardNode; } + async findByIds(ids: EntityId[], depth?: number): Promise { + const boardNode = this.boardNodeRepo.findByIds(ids, depth); + + return boardNode; + } + async findByClassAndId( Constructor: { new (props: S): T }, id: EntityId, diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index 19a37861ea4..be5bba4afa8 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing'; @@ -73,14 +72,26 @@ describe(CardUc.name, () => { const cards = cardFactory.buildList(3); const cardIds = cards.map((c) => c.id); - boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValue( + boardNodeAuthorizableService.getBoardAuthorizables.mockResolvedValue([ new BoardNodeAuthorizable({ users: [], - id: new ObjectId().toHexString(), + id: cards[0].id, boardNode: cards[0], rootNode: columnBoardFactory.build(), - }) - ); + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[1].id, + boardNode: cards[1], + rootNode: columnBoardFactory.build(), + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[2].id, + boardNode: cards[2], + rootNode: columnBoardFactory.build(), + }), + ]); authorizationService.hasPermission.mockReturnValue(true); return { user, cards, cardIds }; @@ -109,7 +120,7 @@ describe(CardUc.name, () => { await uc.findCards(user.id, cardIds); - expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledTimes(3); + expect(boardNodeAuthorizableService.getBoardAuthorizables).toHaveBeenCalledTimes(1); }); it('should call the service to check the user permission', async () => { diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index 9c627d0aa99..6ab2f392766 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; +import { AnyBoardNode, AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() @@ -29,19 +29,14 @@ export class CardUc { const user = await this.authorizationService.getUserWithPermissions(userId); const context: AuthorizationContext = { action: Action.read, requiredPermissions: [] }; - const promises = cards.map((card) => - this.boardNodeAuthorizableService.getBoardAuthorizable(card).then((boardNodeAuthorizable) => { - return { boardNodeAuthorizable, boardNode: card }; - }) - ); - const result = await Promise.all(promises); - - const allowedCards = result.reduce((allowedNodes: Card[], { boardNodeAuthorizable, boardNode }) => { + const boardAuthorizables = await this.boardNodeAuthorizableService.getBoardAuthorizables(cards); + + const allowedCards = boardAuthorizables.reduce((allowedNodes: AnyBoardNode[], boardNodeAuthorizable) => { if (this.authorizationService.hasPermission(user, boardNodeAuthorizable, context)) { - allowedNodes.push(boardNode); + allowedNodes.push(boardNodeAuthorizable.boardNode); } return allowedNodes; - }, []); + }, []) as Card[]; return allowedCards; } diff --git a/package-lock.json b/package-lock.json index 52c01244aa7..d2cbd72d353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13", @@ -5407,6 +5407,14 @@ "node": ">=8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@panva/asn1.js": { "version": "1.0.0", "license": "MIT", @@ -19281,13 +19289,15 @@ } }, "node_modules/prom-client": { - "version": "13.2.0", - "license": "Apache-2.0", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", "dependencies": { + "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" }, "engines": { - "node": ">=10" + "node": "^16 || ^18 || >=20" } }, "node_modules/promise-breaker": { diff --git a/package.json b/package.json index c064843247d..2cad05a7150 100644 --- a/package.json +++ b/package.json @@ -234,7 +234,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13", From b750b8df4dd57e0fb383b6aa75142f1c076a2c72 Mon Sep 17 00:00:00 2001 From: Phillip Date: Wed, 17 Jul 2024 09:48:42 +0200 Subject: [PATCH 30/35] BC-7676 Updated k8s jobs to run as non-root users (#5109) --- .../templates/api-h5p-library-management-cronjob.yml.j2 | 5 +++++ .../templates/api-delete-s3-files-cronjob.yml.j2 | 5 +++++ .../templates/data-deletion-trigger-cronjob.yml.j2 | 5 +++++ .../schulcloud-server-core/templates/migration-job.yml.j2 | 5 +++++ .../templates/tldraw-delete-files-cronjob.yml.j2 | 5 +++++ .../roles/schulcloud-server-init/templates/job_init.yml.j2 | 5 +++++ .../templates/api-ldap-sync-full-cronjob.yml.j2 | 5 +++++ 7 files changed, 35 insertions(+) diff --git a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 index a3290e08f3e..e369044ea85 100644 --- a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 +++ b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 @@ -30,6 +30,11 @@ spec: git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true volumes: - name: libraries-list configMap: 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 84d6f11e4fb..95443ef537b 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 @@ -20,6 +20,11 @@ spec: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: delete-s3-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 index 7f350b86c97..a8c02d02769 100644 --- a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 @@ -29,6 +29,11 @@ spec: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: data-deletion-trigger-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 index f9b76dc34a7..42edd22f4a0 100644 --- a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 @@ -11,6 +11,11 @@ spec: labels: app: api-migration spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: api-migration-job image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 index 80b8e5e5e41..3f702e42e72 100644 --- a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 @@ -20,6 +20,11 @@ spec: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: tldraw-delete-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 index b6c777a1ef2..66d4e563983 100644 --- a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 @@ -7,6 +7,11 @@ metadata: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: api-init image: quay.io/schulcloudverbund/infra-tools:latest 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 41a4d21c783..44e1d95d5c4 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 @@ -20,6 +20,11 @@ spec: activeDeadlineSeconds: {{ SERVER_LDAP_SYNC_FULL_CRONJOB_TIMEOUT|default("39600", true) }} template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: api-ldapsync-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} From 216adda46f23fefed2bc5ee229219735545e4553 Mon Sep 17 00:00:00 2001 From: Cedric Evers <12080057+CeEv@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:19:24 +0200 Subject: [PATCH 31/35] BC-7655 cleanup shared (#5107) * Move ConverterUtil .xml2object to bbb module. * Move jwt methodes to shared. * Move GuardAgainst to TypeGuard --- .../authorization-client.adapter.ts | 31 ++--- .../decorator/auth.decorator.ts | 6 +- .../services/authentication.service.ts | 2 +- .../authentication/strategy/jwt.strategy.ts | 9 +- .../authentication/strategy/local.strategy.ts | 12 +- .../strategy/ws-jwt.strategy.ts | 2 +- .../video-conference/bbb/bbb.service.spec.ts | 113 +++++++++--------- .../video-conference/bbb/bbb.service.ts | 79 ++++++------ .../video-conference.module.ts | 2 - .../shared/common/guards/type.guard.spec.ts | 92 ++++++++++++++ .../src/shared/common/guards/type.guard.ts | 24 ++++ .../common/utils/converter.util.spec.ts | 22 ---- .../src/shared/common/utils/converter.util.ts | 12 -- .../shared/common/utils/guard-against.spec.ts | 32 ----- .../src/shared/common/utils/guard-against.ts | 14 --- apps/server/src/shared/common/utils/index.ts | 4 +- .../common/utils/jwt.spec.ts} | 2 +- .../common/utils/jwt.ts} | 9 +- 18 files changed, 238 insertions(+), 229 deletions(-) delete mode 100644 apps/server/src/shared/common/utils/converter.util.spec.ts delete mode 100644 apps/server/src/shared/common/utils/converter.util.ts delete mode 100644 apps/server/src/shared/common/utils/guard-against.spec.ts delete mode 100644 apps/server/src/shared/common/utils/guard-against.ts rename apps/server/src/{modules/authentication/helper/jwt-extractor.spec.ts => shared/common/utils/jwt.spec.ts} (95%) rename apps/server/src/{modules/authentication/helper/jwt-extractor.ts => shared/common/utils/jwt.ts} (60%) diff --git a/apps/server/src/infra/authorization-client/authorization-client.adapter.ts b/apps/server/src/infra/authorization-client/authorization-client.adapter.ts index daf155fdf14..711aec8717f 100644 --- a/apps/server/src/infra/authorization-client/authorization-client.adapter.ts +++ b/apps/server/src/infra/authorization-client/authorization-client.adapter.ts @@ -1,9 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { RawAxiosRequestConfig } from 'axios'; -import cookie from 'cookie'; import { Request } from 'express'; -import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt'; +import { extractJwtFromHeader } from '@shared/common'; import { AuthorizationApi, AuthorizationBodyParams } from './authorization-api-client'; import { AuthorizationErrorLoggableException, AuthorizationForbiddenLoggableException } from './error'; @@ -19,9 +18,9 @@ export class AuthorizationClientAdapter { } public async hasPermissionsByReference(params: AuthorizationBodyParams): Promise { - const options = this.createOptionParams(params); - try { + const options = this.createOptionParams(); + const response = await this.authorizationApi.authorizationReferenceControllerAuthorizeByReference( params, options @@ -34,34 +33,20 @@ export class AuthorizationClientAdapter { } } - private createOptionParams(params: AuthorizationBodyParams): RawAxiosRequestConfig { - const jwt = this.getJWT(params); + private createOptionParams(): RawAxiosRequestConfig { + const jwt = this.getJwt(); const options: RawAxiosRequestConfig = { headers: { authorization: `Bearer ${jwt}` } }; return options; } - private getJWT(params: AuthorizationBodyParams): string { - const getJWT = ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), this.fromCookie('jwt')]); - const jwt = getJWT(this.request) || this.request.headers.authorization; + private getJwt(): string { + const jwt = extractJwtFromHeader(this.request) || this.request.headers.authorization; if (!jwt) { - const error = new Error('Authentication is required.'); - throw new AuthorizationErrorLoggableException(error, params); + throw new Error('Authentication is required.'); } return jwt; } - - private fromCookie(name: string): JwtFromRequestFunction { - return (request: Request) => { - let token: string | null = null; - const cookies = cookie.parse(request.headers.cookie || ''); - if (cookies && cookies[name]) { - token = cookies[name]; - } - - return token; - }; - } } diff --git a/apps/server/src/modules/authentication/decorator/auth.decorator.ts b/apps/server/src/modules/authentication/decorator/auth.decorator.ts index 0bcfb55abc4..583799977f2 100644 --- a/apps/server/src/modules/authentication/decorator/auth.decorator.ts +++ b/apps/server/src/modules/authentication/decorator/auth.decorator.ts @@ -8,10 +8,9 @@ import { } from '@nestjs/common'; import { ApiBearerAuth } from '@nestjs/swagger'; import { Request } from 'express'; -import { ExtractJwt } from 'passport-jwt'; +import { extractJwtFromHeader } from '@shared/common'; import { JwtAuthGuard } from '../guard/jwt-auth.guard'; import { ICurrentUser, isICurrentUser } from '../interface/user'; -import { JwtExtractor } from '../helper/jwt-extractor'; const STRATEGIES = ['jwt'] as const; type Strategies = typeof STRATEGIES; @@ -56,9 +55,8 @@ export const CurrentUser = createParamDecorator((_, * @requires Authenticated */ export const JWT = createParamDecorator((_, ctx: ExecutionContext) => { - const getJWT = ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), JwtExtractor.fromCookie('jwt')]); const req: Request = ctx.switchToHttp().getRequest(); - const jwt = getJWT(req) || req.headers.authorization; + const jwt = extractJwtFromHeader(req) || req.headers.authorization; if (!jwt) { throw new UnauthorizedException('Authentication is required.'); diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index 124a4c419b8..b2ecb5f4609 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -44,7 +44,7 @@ export class AuthenticationService { async generateJwt(user: CreateJwtPayload): Promise { const jti = randomUUID(); - const result: LoginDto = new LoginDto({ + const result = new LoginDto({ accessToken: this.jwtService.sign(user, { subject: user.accountId, jwtid: jti, diff --git a/apps/server/src/modules/authentication/strategy/jwt.strategy.ts b/apps/server/src/modules/authentication/strategy/jwt.strategy.ts index 0dbe9e39bf4..84014531093 100644 --- a/apps/server/src/modules/authentication/strategy/jwt.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/jwt.strategy.ts @@ -1,21 +1,18 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Strategy } from 'passport-jwt'; +import { extractJwtFromHeader } from '@shared/common'; import { jwtConstants } from '../constants'; import { ICurrentUser } from '../interface'; import { JwtPayload } from '../interface/jwt-payload'; import { CurrentUserMapper } from '../mapper'; -import { JwtExtractor } from '../helper/jwt-extractor'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private readonly jwtValidationAdapter: JwtValidationAdapter) { super({ - jwtFromRequest: ExtractJwt.fromExtractors([ - ExtractJwt.fromAuthHeaderAsBearerToken(), - JwtExtractor.fromCookie('jwt'), - ]), + jwtFromRequest: extractJwtFromHeader, ignoreExpiration: false, secretOrKey: jwtConstants.secret, ...jwtConstants.jwtOptions, diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.ts b/apps/server/src/modules/authentication/strategy/local.strategy.ts index 3e762219f7d..30e3f191b2c 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.ts @@ -3,7 +3,7 @@ import { Account } from '@modules/account'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import { GuardAgainst } from '@shared/common/utils/guard-against'; +import { TypeGuard } from '@shared/common'; import { UserRepo } from '@shared/repo'; import bcrypt from 'bcryptjs'; import { Strategy } from 'passport-local'; @@ -28,13 +28,13 @@ export class LocalStrategy extends PassportStrategy(Strategy) { if (this.configService.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED')) { const jwt = await this.idmOauthService.resourceOwnerPasswordGrant(username, password); - GuardAgainst.nullOrUndefined(jwt, new UnauthorizedException()); + TypeGuard.checkNotNullOrUndefined(jwt, new UnauthorizedException()); } else { - const accountPassword = GuardAgainst.nullOrUndefined(account.password, new UnauthorizedException()); + const accountPassword = TypeGuard.checkNotNullOrUndefined(account.password, new UnauthorizedException()); await this.checkCredentials(password, accountPassword, account); } - const accountUserId = GuardAgainst.nullOrUndefined( + const accountUserId = TypeGuard.checkNotNullOrUndefined( account.userId, new Error(`login failing, because account ${account.id} has no userId`) ); @@ -44,8 +44,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) { } private cleanupInput(username?: string, password?: string): { username: string; password: string } { - username = GuardAgainst.nullOrUndefined(username, new UnauthorizedException()); - password = GuardAgainst.nullOrUndefined(password, new UnauthorizedException()); + username = TypeGuard.checkNotNullOrUndefined(username, new UnauthorizedException()); + password = TypeGuard.checkNotNullOrUndefined(password, new UnauthorizedException()); username = this.authenticationService.normalizeUsername(username); password = this.authenticationService.normalizePassword(password); return { username, password }; diff --git a/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts b/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts index ef9bf54b67c..ea76a267da0 100644 --- a/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts @@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { WsException } from '@nestjs/websockets'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtExtractor } from '@shared/common'; import { jwtConstants } from '../constants'; import { ICurrentUser } from '../interface'; import { JwtPayload } from '../interface/jwt-payload'; import { CurrentUserMapper } from '../mapper'; -import { JwtExtractor } from '../helper/jwt-extractor'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; @Injectable() diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts index 9c643f80698..258f06f10c6 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts @@ -3,7 +3,6 @@ import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConverterUtil } from '@shared/common'; import { axiosResponseFactory } from '@shared/testing'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; @@ -112,7 +111,6 @@ describe(BBBService.name, () => { let module: TestingModule; let service: BBBServiceTest; let httpService: DeepMocked; - let converterUtil: DeepMocked; let configService: DeepMocked>; beforeAll(async () => { @@ -127,15 +125,10 @@ describe(BBBService.name, () => { provide: HttpService, useValue: createMock(), }, - { - provide: ConverterUtil, - useValue: createMock(), - }, ], }).compile(); service = module.get(BBBServiceTest); httpService = module.get(HttpService); - converterUtil = module.get(ConverterUtil); configService = module.get(ConfigService); }); @@ -147,6 +140,10 @@ describe(BBBService.name, () => { configService.get.mockReturnValue('https://mocked'); }); + afterEach(() => { + jest.resetAllMocks(); + }); + describe('create', () => { describe('when valid parameter passed and the BBB response well', () => { const setup = () => { @@ -156,20 +153,20 @@ describe(BBBService.name, () => { const param = createBBBCreateConfig(); - httpService.post.mockReturnValue(of(bbbCreateResponse)); - converterUtil.xml2object.mockReturnValue(bbbCreateResponse.data); + httpService.post.mockReturnValueOnce(of(bbbCreateResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbCreateResponse.data); - return { param, bbbCreateResponse }; + return { param, bbbCreateResponse, spy }; }; it('should return a response with returncode success', async () => { - const { bbbCreateResponse, param } = setup(); + const { bbbCreateResponse, param, spy } = setup(); const result = await service.create(param); expect(result).toBeDefined(); expect(httpService.post).toHaveBeenCalledTimes(1); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbCreateResponse.data); + expect(spy).toHaveBeenCalledWith(bbbCreateResponse.data); }); }); @@ -182,8 +179,8 @@ describe(BBBService.name, () => { const param = createBBBCreateConfig(); - httpService.post.mockReturnValue(of(bbbCreateResponse)); - converterUtil.xml2object.mockReturnValue(bbbCreateResponse.data); + httpService.post.mockReturnValueOnce(of(bbbCreateResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbCreateResponse.data); const error = new InternalServerErrorException( `${bbbCreateResponse.data.response.messageKey}, ${bbbCreateResponse.data.response.message}` @@ -202,16 +199,6 @@ describe(BBBService.name, () => { await expect(service.create(param)).rejects.toThrowError(expectedError); }); }); - - it('should return a xml configuration with provided presentation url', () => { - const presentationUrl = 'https://s3.hidrive.strato.com/cloud-instances/bbb/presentation.pdf'; - - const result = service.getBbbRequestConfig(presentationUrl); - - expect(result).toBe( - "" - ); - }); }); describe('end', () => { @@ -222,20 +209,20 @@ describe(BBBService.name, () => { ); const bbbBaseMeetingConfig: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbBaseResponse)); - converterUtil.xml2object.mockReturnValue(bbbBaseResponse.data); + httpService.get.mockReturnValueOnce(of(bbbBaseResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbBaseResponse.data); - return { bbbBaseResponse, bbbBaseMeetingConfig }; + return { bbbBaseResponse, bbbBaseMeetingConfig, spy }; }; it('should return a response with returncode success', async () => { - const { bbbBaseResponse, bbbBaseMeetingConfig } = setup(); + const { bbbBaseResponse, bbbBaseMeetingConfig, spy } = setup(); const result = await service.end(bbbBaseMeetingConfig); expect(result).toBeDefined(); expect(httpService.get).toBeCalled(); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbBaseResponse.data); + expect(spy).toHaveBeenCalledWith(bbbBaseResponse.data); }); }); @@ -247,8 +234,8 @@ describe(BBBService.name, () => { bbbBaseResponse.data.response.returncode = BBBStatus.ERROR; const param: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbBaseResponse)); - converterUtil.xml2object.mockReturnValue(bbbBaseResponse.data); + httpService.get.mockReturnValueOnce(of(bbbBaseResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbBaseResponse.data); const error = new InternalServerErrorException( `${bbbBaseResponse.data.response.messageKey}, ${bbbBaseResponse.data.response.message}` @@ -277,19 +264,19 @@ describe(BBBService.name, () => { ); const param: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); - return { bbbMeetingInfoResponse, param }; + return { bbbMeetingInfoResponse, param, spy }; }; it('should return a response with returncode success', async () => { - const { bbbMeetingInfoResponse, param } = setup(); + const { bbbMeetingInfoResponse, param, spy } = setup(); const result = await service.getMeetingInfo(param); expect(result).toBeDefined(); expect(httpService.get).toBeCalled(); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); + expect(spy).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); }); }); @@ -301,8 +288,8 @@ describe(BBBService.name, () => { bbbMeetingInfoResponse.data.response.returncode = BBBStatus.ERROR; const param: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); const error = new InternalServerErrorException( `${bbbMeetingInfoResponse.data.response.messageKey}: ${bbbMeetingInfoResponse.data.response.message}` @@ -331,20 +318,20 @@ describe(BBBService.name, () => { ); const param: BBBJoinConfig = createBBBJoinConfig(); - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); - return { param, bbbMeetingInfoResponse }; + return { param, bbbMeetingInfoResponse, spy }; }; it('should create a join link to a bbb meeting', async () => { - const { param, bbbMeetingInfoResponse } = setup(); + const { param, bbbMeetingInfoResponse, spy } = setup(); const url = await service.join(param); expect(url).toBeDefined(); expect(httpService.get).toBeCalled(); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); + expect(spy).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); }); }); @@ -356,8 +343,8 @@ describe(BBBService.name, () => { bbbMeetingInfoResponse.data.response.returncode = BBBStatus.ERROR; const param: BBBJoinConfig = createBBBJoinConfig(); - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); const error = new InternalServerErrorException( `${bbbMeetingInfoResponse.data.response.messageKey}: ${bbbMeetingInfoResponse.data.response.message}` @@ -378,9 +365,9 @@ describe(BBBService.name, () => { }); it('toParams: should return params based on bbb configs', () => { - const createConfig: BBBCreateConfig = createBBBCreateConfig(); + const createConfig = createBBBCreateConfig(); - const params: URLSearchParams = service.superToParams(createConfig); + const params = service.superToParams(createConfig); expect(params.get('name')).toEqual(createConfig.name); expect(params.get('meetingID')).toEqual(createConfig.meetingID); @@ -392,32 +379,40 @@ describe(BBBService.name, () => { expect(params.get('allowModsToUnmuteUsers')).toEqual(String(createConfig.allowModsToUnmuteUsers)); }); - it('generateChecksum: should generate a checksum for queryParams', () => { - // Arrange + const setup = () => { const hashMock: Hash = { update: jest.fn().mockReturnThis(), - digest: jest.fn().mockReturnValue('encrypt 123'), + digest: jest.fn().mockReturnValueOnce('encrypt 123').mockReturnValueOnce('encrypt 123'), } as unknown as Hash; const createHashMock = jest.spyOn(crypto, 'createHash').mockImplementation((): Hash => hashMock); - const createConfig: BBBCreateConfig = createBBBCreateConfig(); + const createConfig = createBBBCreateConfig(); const callName = 'create'; - const urlSearchParams: URLSearchParams = service.superToParams(createConfig); - const queryString: string = urlSearchParams.toString(); + + return { + callName, + createConfig, + createHashMock, + }; + }; + + it('generateChecksum: should generate a checksum for queryParams', () => { + const { callName, createConfig, createHashMock } = setup(); + const urlSearchParams = service.superToParams(createConfig); + const queryString = urlSearchParams.toString(); const sha = crypto.createHash('sha1'); - const expectedChecksum: string = sha.update(callName + queryString + service.getSalt()).digest('hex'); + const expectedChecksum = sha.update(callName + queryString + service.getSalt()).digest('hex'); - const checksum: string = service.superGenerateChecksum(callName, urlSearchParams); + const checksum = service.superGenerateChecksum(callName, urlSearchParams); expect(checksum).toEqual(expectedChecksum); expect(createHashMock).toBeCalledWith('sha1'); }); it('getUrl: should return composed url', () => { - const createConfig = createBBBCreateConfig(); - const callName = 'create'; - const params: URLSearchParams = service.superToParams(createConfig); + const { callName, createConfig } = setup(); + const params = service.superToParams(createConfig); - const url: string = service.superGetUrl(callName, params); + const url = service.superGetUrl(callName, params); expect(url.toString()).toContain(`${service.getBaseUrl()}/bigbluebutton/api/${callName}`); expect(url.includes('checksum')).toBeTruthy(); diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.ts index b40e47053b4..86e2f972ef3 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.ts @@ -1,12 +1,12 @@ import { HttpService } from '@nestjs/axios'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { ConverterUtil } from '@shared/common/utils'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; import crypto from 'crypto'; import { firstValueFrom, Observable } from 'rxjs'; import { URL, URLSearchParams } from 'url'; +import xml2json from '@hendt/xml2json/lib'; import { VideoConferenceConfig } from '../video-conference-config'; import { BbbConfig } from './bbb-config'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig } from './request'; @@ -16,8 +16,7 @@ import { BBBBaseResponse, BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse export class BBBService { constructor( private readonly configService: ConfigService, - private readonly httpService: HttpService, - private readonly converterUtil: ConverterUtil + private readonly httpService: HttpService ) {} protected get baseUrl(): string { @@ -32,13 +31,26 @@ export class BBBService { return this.configService.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION'); } + /** Note no guard, or type check. Should be private. */ + public xml2object(xml: string): T { + const json = xml2json(xml) as T; + + return json; + } + + private checkIfResponseSucces( + bbbResp: BBBResponse | BBBResponse | BBBResponse + ): void { + if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { + throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); + } + } + /** * Creates a new BBB Meeting. The create call is idempotent: you can call it multiple times with the same parameters without side effects. - * @param {BBBCreateConfig} config - * @returns {Promise>} * @throws {InternalServerErrorException} */ - create(config: BBBCreateConfig): Promise> { + public create(config: BBBCreateConfig): Promise> { const url: string = this.getUrl('create', this.toParams(config)); const conf = { headers: { 'Content-Type': 'application/xml' } }; const data = this.getBbbRequestConfig(this.presentationUrl); @@ -46,13 +58,10 @@ export class BBBService { return firstValueFrom(observable) .then((resp: AxiosResponse) => { - const bbbResp = this.converterUtil.xml2object | BBBResponse>( - resp.data - ); - if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { - throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); - } - return bbbResp as BBBResponse; + const bbbResp = this.xml2object>(resp.data); + this.checkIfResponseSucces(bbbResp); + + return bbbResp; }) .catch((error) => { throw new InternalServerErrorException(null, ErrorUtils.createHttpExceptionOptions(error, 'BBBService:create')); @@ -60,7 +69,7 @@ export class BBBService { } // it should be a private method - getBbbRequestConfig(presentationUrl: string): string { + private getBbbRequestConfig(presentationUrl: string): string { if (presentationUrl === '') return ''; return ``; } @@ -71,7 +80,7 @@ export class BBBService { * @returns {Promise} The join url * @throws {InternalServerErrorException} */ - async join(config: BBBJoinConfig): Promise { + public async join(config: BBBJoinConfig): Promise { await this.getMeetingInfo(new BBBBaseMeetingConfig({ meetingID: config.meetingID })); return this.getUrl('join', this.toParams(config)); @@ -83,16 +92,15 @@ export class BBBService { * @returns {BBBResponse} * @throws {InternalServerErrorException} */ - end(config: BBBBaseMeetingConfig): Promise> { + public end(config: BBBBaseMeetingConfig): Promise> { const url: string = this.getUrl('end', this.toParams(config)); const observable: Observable> = this.httpService.get(url); return firstValueFrom(observable) .then((resp: AxiosResponse) => { - const bbbResp = this.converterUtil.xml2object>(resp.data); - if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { - throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); - } + const bbbResp = this.xml2object>(resp.data); + this.checkIfResponseSucces(bbbResp); + return bbbResp; }) .catch((error) => { @@ -102,23 +110,18 @@ export class BBBService { /** * Returns information about a BBB Meeting. - * @param {BBBBaseMeetingConfig} config - * @returns {Promise} * @throws {InternalServerErrorException} */ - getMeetingInfo(config: BBBBaseMeetingConfig): Promise> { + public getMeetingInfo(config: BBBBaseMeetingConfig): Promise> { const url: string = this.getUrl('getMeetingInfo', this.toParams(config)); const observable: Observable> = this.httpService.get(url); return firstValueFrom(observable) .then((resp: AxiosResponse) => { - const bbbResp = this.converterUtil.xml2object< - BBBResponse | BBBResponse - >(resp.data); - if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { - throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); - } - return bbbResp as BBBResponse; + const bbbResp = this.xml2object>(resp.data); + this.checkIfResponseSucces(bbbResp); + + return bbbResp; }) .catch((error) => { throw new InternalServerErrorException( @@ -128,26 +131,22 @@ export class BBBService { }); } - // should be private /** * Returns a SHA1 encoded checksum for the input parameters. - * @param {string} callName - * @param {URLSearchParams} queryParams - * @returns {string} + * should be private */ protected generateChecksum(callName: string, queryParams: URLSearchParams): string { const queryString: string = queryParams.toString(); const sha = crypto.createHash('sha1'); sha.update(callName + queryString + this.salt); const checksum: string = sha.digest('hex'); + return checksum; } - // should be private /** * Extracts fields from a javascript object and builds a URLSearchParams object from it. - * @param {object} object - * @returns {URLSearchParams} + * should be private */ protected toParams(object: BBBCreateConfig | BBBBaseMeetingConfig): URLSearchParams { const params: URLSearchParams = new URLSearchParams(); @@ -156,15 +155,13 @@ export class BBBService { params.append(key, String(object[key])); } }); + return params; } - // should be private /** * Builds the url for BBB. - * @param callName Name of the BBB api function. - * @param queryParams Parameters for the endpoint. - * @returns {string} A callable url. + * should be private */ protected getUrl(callName: string, queryParams: URLSearchParams): string { const checksum: string = this.generateChecksum(callName, queryParams); 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 0b7bf02841a..72c2be9fb6e 100644 --- a/apps/server/src/modules/video-conference/video-conference.module.ts +++ b/apps/server/src/modules/video-conference/video-conference.module.ts @@ -5,7 +5,6 @@ import { LegacySchoolModule } from '@modules/legacy-school'; import { UserModule } from '@modules/user'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { ConverterUtil } from '@shared/common'; import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; import { LoggerModule } from '@src/core/logger'; @@ -32,7 +31,6 @@ import { VideoConferenceDeprecatedUc } from './uc'; VideoConferenceRepo, // TODO: N21-1010 clean up video conferences - remove repos TeamsRepo, - ConverterUtil, VideoConferenceService, // TODO: N21-885 remove VideoConferenceDeprecatedUc from providers VideoConferenceDeprecatedUc, diff --git a/apps/server/src/shared/common/guards/type.guard.spec.ts b/apps/server/src/shared/common/guards/type.guard.spec.ts index 9f129244685..fdf31d42889 100644 --- a/apps/server/src/shared/common/guards/type.guard.spec.ts +++ b/apps/server/src/shared/common/guards/type.guard.spec.ts @@ -212,4 +212,96 @@ describe('TypeGuard', () => { }); }); }); + + describe('isNull', () => { + describe('when passing type of value is null', () => { + it('should be return true', () => { + expect(TypeGuard.isNull(null)).toBe(true); + }); + }); + + describe('when passing type of value is NOT null', () => { + it('should be return false', () => { + expect(TypeGuard.isNull(undefined)).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isNull('string')).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isNull('')).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isNull({})).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isNull(1)).toBe(false); + }); + }); + }); + + describe('isUndefined', () => { + describe('when passing type of value is undefined', () => { + it('should be return true', () => { + expect(TypeGuard.isUndefined(undefined)).toBe(true); + }); + }); + + describe('when passing type of value is NOT undefined', () => { + it('should be return false', () => { + expect(TypeGuard.isUndefined(null)).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isUndefined('string')).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isUndefined('')).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isUndefined({})).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isUndefined(1)).toBe(false); + }); + }); + }); + + describe('checkNotNullOrUndefined', () => { + describe('when value is null', () => { + it('should throw error if it is passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(null, new Error('Test'))).toThrow('Test'); + }); + + it('should throw default error if not error passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(null)).toThrow('Type is null.'); + }); + }); + + describe('when value is undefined', () => { + it('should throw error if it is passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(undefined, new Error('Test'))).toThrow('Test'); + }); + + it('should throw default error if not error passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(undefined)).toThrow('Type is undefined.'); + }); + }); + + describe('when value is defined', () => { + it('should return value if error is passed', () => { + expect(TypeGuard.checkNotNullOrUndefined('', new Error('Test'))).toBe(''); + }); + + it('should return value', () => { + expect(TypeGuard.checkNotNullOrUndefined('')).toBe(''); + }); + }); + }); }); diff --git a/apps/server/src/shared/common/guards/type.guard.ts b/apps/server/src/shared/common/guards/type.guard.ts index 171ca40b596..f7500085d2f 100644 --- a/apps/server/src/shared/common/guards/type.guard.ts +++ b/apps/server/src/shared/common/guards/type.guard.ts @@ -46,4 +46,28 @@ export class TypeGuard { return isObject; } + + static isNull(value: unknown): value is null { + const isNull = value === null; + + return isNull; + } + + static isUndefined(value: unknown): value is undefined { + const isUndefined = value === undefined; + + return isUndefined; + } + + static checkNotNullOrUndefined(value: T | null | undefined, toThrow?: Error): T { + if (TypeGuard.isNull(value)) { + throw toThrow || new Error('Type is null.'); + } + + if (TypeGuard.isUndefined(value)) { + throw toThrow || new Error('Type is undefined.'); + } + + return value; + } } diff --git a/apps/server/src/shared/common/utils/converter.util.spec.ts b/apps/server/src/shared/common/utils/converter.util.spec.ts deleted file mode 100644 index 004166c7c28..00000000000 --- a/apps/server/src/shared/common/utils/converter.util.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ConverterUtil } from '@shared/common'; - -class TestObject { - test: string; - - constructor(test: string) { - this.test = test; - } -} -describe('ConverterUtil', () => { - let service: ConverterUtil; - beforeAll(() => { - service = new ConverterUtil(); - }); - describe('xml2Object', () => { - it('should map correctly to TestObject', () => { - const test = 'test'; - const ret = service.xml2object(test); - expect(ret.test).toEqual('test'); - }); - }); -}); diff --git a/apps/server/src/shared/common/utils/converter.util.ts b/apps/server/src/shared/common/utils/converter.util.ts deleted file mode 100644 index 814bdfda096..00000000000 --- a/apps/server/src/shared/common/utils/converter.util.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import xml2json from '@hendt/xml2json'; - -/** - * This class encapsulates - */ -@Injectable() -export class ConverterUtil { - xml2object(xml: string): T { - return xml2json(xml) as T; - } -} diff --git a/apps/server/src/shared/common/utils/guard-against.spec.ts b/apps/server/src/shared/common/utils/guard-against.spec.ts deleted file mode 100644 index 0f46e6e96b6..00000000000 --- a/apps/server/src/shared/common/utils/guard-against.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GuardAgainst } from '@shared/common'; - -describe('GuardAgainst', () => { - describe('nullOrUndefined', () => { - describe('when value is null', () => { - const error = new Error('value is null'); - - it('should throw', () => { - const value: string | null = null; - expect(() => GuardAgainst.nullOrUndefined(value, error)).toThrow(error.message); - }); - }); - - describe('when value is undefined', () => { - const error = new Error('value is undefined'); - - it('should throw', () => { - const value: string | undefined = undefined; - expect(() => GuardAgainst.nullOrUndefined(value, error)).toThrow(error.message); - }); - }); - - describe('when value is defined', () => { - const error = new Error('value is null'); - - it('should return value', () => { - const value = ''; - expect(GuardAgainst.nullOrUndefined(value, error)).toBe(''); - }); - }); - }); -}); diff --git a/apps/server/src/shared/common/utils/guard-against.ts b/apps/server/src/shared/common/utils/guard-against.ts deleted file mode 100644 index 6425399681a..00000000000 --- a/apps/server/src/shared/common/utils/guard-against.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class GuardAgainst { - /** - * Guards against null or undefined and throws specified exception. - * @param value The value to check. - * @param toThrow The exception to be thrown on failure. - * @returns The narrowed value or throws. - */ - static nullOrUndefined(value: T | null | undefined, toThrow: unknown): T | never { - if (value === null || value === undefined) { - throw toThrow; - } - return value; - } -} diff --git a/apps/server/src/shared/common/utils/index.ts b/apps/server/src/shared/common/utils/index.ts index 90dadf998ed..6c6e6ffe2be 100644 --- a/apps/server/src/shared/common/utils/index.ts +++ b/apps/server/src/shared/common/utils/index.ts @@ -1,3 +1,3 @@ -export * from './converter.util'; -export * from './guard-against'; export { SortHelper } from './sort-helper'; +export { getResolvedValues, isFulfilled } from './promise'; +export { extractJwtFromHeader, JwtExtractor } from './jwt'; diff --git a/apps/server/src/modules/authentication/helper/jwt-extractor.spec.ts b/apps/server/src/shared/common/utils/jwt.spec.ts similarity index 95% rename from apps/server/src/modules/authentication/helper/jwt-extractor.spec.ts rename to apps/server/src/shared/common/utils/jwt.spec.ts index 8186287171a..f0d9a74f22f 100644 --- a/apps/server/src/modules/authentication/helper/jwt-extractor.spec.ts +++ b/apps/server/src/shared/common/utils/jwt.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Request } from 'express'; import { JwtFromRequestFunction } from 'passport-jwt'; -import { JwtExtractor } from './jwt-extractor'; +import { JwtExtractor } from './jwt'; describe('JwtExtractor', () => { let request: DeepMocked; diff --git a/apps/server/src/modules/authentication/helper/jwt-extractor.ts b/apps/server/src/shared/common/utils/jwt.ts similarity index 60% rename from apps/server/src/modules/authentication/helper/jwt-extractor.ts rename to apps/server/src/shared/common/utils/jwt.ts index d54807c2ac2..ebc589236dc 100644 --- a/apps/server/src/modules/authentication/helper/jwt-extractor.ts +++ b/apps/server/src/shared/common/utils/jwt.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { JwtFromRequestFunction } from 'passport-jwt'; +import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt'; import cookie from 'cookie'; export class JwtExtractor { @@ -7,12 +7,15 @@ export class JwtExtractor { return (request: Request) => { let token: string | null = null; const cookies = cookie.parse(request.headers.cookie || ''); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (cookies && cookies[name]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access token = cookies[name]; } return token; }; } } + +export const extractJwtFromHeader = ExtractJwt.fromExtractors([ + ExtractJwt.fromAuthHeaderAsBearerToken(), + JwtExtractor.fromCookie('jwt'), +]); From 165a3be26e0751dda63fbbef85f49698e2b4c0e7 Mon Sep 17 00:00:00 2001 From: mamutmk5 <3045922+mamutmk5@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:31:25 +0200 Subject: [PATCH 32/35] Revert "BC-7676 Updated k8s jobs to run as non-root users (#5109)" (#5112) This reverts commit b750b8df4dd57e0fb383b6aa75142f1c076a2c72. --- .../templates/api-h5p-library-management-cronjob.yml.j2 | 5 ----- .../templates/api-delete-s3-files-cronjob.yml.j2 | 5 ----- .../templates/data-deletion-trigger-cronjob.yml.j2 | 5 ----- .../schulcloud-server-core/templates/migration-job.yml.j2 | 5 ----- .../templates/tldraw-delete-files-cronjob.yml.j2 | 5 ----- .../roles/schulcloud-server-init/templates/job_init.yml.j2 | 5 ----- .../templates/api-ldap-sync-full-cronjob.yml.j2 | 5 ----- 7 files changed, 35 deletions(-) diff --git a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 index e369044ea85..a3290e08f3e 100644 --- a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 +++ b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 @@ -30,11 +30,6 @@ spec: git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true volumes: - name: libraries-list configMap: 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 95443ef537b..84d6f11e4fb 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 @@ -20,11 +20,6 @@ spec: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: delete-s3-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 index a8c02d02769..7f350b86c97 100644 --- a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 @@ -29,11 +29,6 @@ spec: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: data-deletion-trigger-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 index 42edd22f4a0..f9b76dc34a7 100644 --- a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 @@ -11,11 +11,6 @@ spec: labels: app: api-migration spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: api-migration-job image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 index 3f702e42e72..80b8e5e5e41 100644 --- a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 @@ -20,11 +20,6 @@ spec: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: tldraw-delete-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 index 66d4e563983..b6c777a1ef2 100644 --- a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 @@ -7,11 +7,6 @@ metadata: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: api-init image: quay.io/schulcloudverbund/infra-tools:latest 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 44e1d95d5c4..41a4d21c783 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 @@ -20,11 +20,6 @@ spec: activeDeadlineSeconds: {{ SERVER_LDAP_SYNC_FULL_CRONJOB_TIMEOUT|default("39600", true) }} template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: api-ldapsync-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} From 302d8b29b6c02c2b0887d74521edbdb0da757746 Mon Sep 17 00:00:00 2001 From: Phillip Date: Thu, 18 Jul 2024 12:24:27 +0200 Subject: [PATCH 33/35] BC-7676 Updated k8s jobs to run as non-root users where easily possible (#5116) --- .../templates/api-h5p-library-management-cronjob.yml.j2 | 5 +++++ .../templates/api-delete-s3-files-cronjob.yml.j2 | 5 +++++ .../templates/data-deletion-trigger-cronjob.yml.j2 | 5 +++++ .../schulcloud-server-core/templates/migration-job.yml.j2 | 5 +++++ .../templates/tldraw-delete-files-cronjob.yml.j2 | 5 +++++ .../roles/schulcloud-server-init/templates/job_init.yml.j2 | 2 +- 6 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 index a3290e08f3e..e369044ea85 100644 --- a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 +++ b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 @@ -30,6 +30,11 @@ spec: git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true volumes: - name: libraries-list configMap: 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 84d6f11e4fb..95443ef537b 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 @@ -20,6 +20,11 @@ spec: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: delete-s3-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 index 7f350b86c97..a8c02d02769 100644 --- a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 @@ -29,6 +29,11 @@ spec: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: data-deletion-trigger-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 index f9b76dc34a7..42edd22f4a0 100644 --- a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 @@ -11,6 +11,11 @@ spec: labels: app: api-migration spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: api-migration-job image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 index 80b8e5e5e41..3f702e42e72 100644 --- a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 @@ -20,6 +20,11 @@ spec: spec: template: spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true containers: - name: tldraw-delete-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 index b6c777a1ef2..ffd8bc98ae5 100644 --- a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 @@ -27,7 +27,7 @@ spec: mountPath: /update.sh subPath: update.sh command: ['/bin/sh','-c'] - args: ['cp /update.sh /update.run.sh && chmod +x /update.run.sh &&./update.run.sh'] + args: ['cp /update.sh /update.run.sh && chmod +x /update.run.sh && ./update.run.sh'] resources: limits: cpu: "3000m" From 41b019b284e573e6c500badc4dc0a0c11b534c46 Mon Sep 17 00:00:00 2001 From: Max Bischof <106820326+bischofmax@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:20:16 +0200 Subject: [PATCH 34/35] BC-7548 - Npm audit fix without mikro orm and h5p server (#5111) --- .../api-test/deletion-executions.api.spec.ts | 3 +- package-lock.json | 1665 ++++++----------- 2 files changed, 531 insertions(+), 1137 deletions(-) diff --git a/apps/server/src/modules/deletion/api/controller/api-test/deletion-executions.api.spec.ts b/apps/server/src/modules/deletion/api/controller/api-test/deletion-executions.api.spec.ts index e10e7488359..e4e5e06f259 100644 --- a/apps/server/src/modules/deletion/api/controller/api-test/deletion-executions.api.spec.ts +++ b/apps/server/src/modules/deletion/api/controller/api-test/deletion-executions.api.spec.ts @@ -35,11 +35,10 @@ describe(`deletionExecution (api)`, () => { describe('executeDeletions', () => { describe('when execute deletionRequests with default limit', () => { - jest.setTimeout(20000); it('should return status 204', async () => { const response = await testApiClient.post(''); expect(response.status).toEqual(204); - }); + }, 20000); }); describe('without token', () => { diff --git a/package-lock.json b/package-lock.json index d2cbd72d353..9ff6ca29b99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1717,10 +1717,11 @@ } }, "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.49.0", - "license": "Apache-2.0", + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", "dependencies": { - "tslib": "^2.3.0" + "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/util-waiter": { @@ -2479,6 +2480,14 @@ "npm": ">=6.14.13" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@feathersjs/adapter-commons": { "version": "5.0.12", "license": "MIT", @@ -2937,8 +2946,9 @@ "license": "MIT" }, "node_modules/@httptoolkit/websocket-stream": { - "version": "6.0.0", - "license": "BSD-2-Clause", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@httptoolkit/websocket-stream/-/websocket-stream-6.0.1.tgz", + "integrity": "sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==", "optional": true, "peer": true, "dependencies": { @@ -2953,8 +2963,9 @@ } }, "node_modules/@httptoolkit/websocket-stream/node_modules/readable-stream": { - "version": "2.3.7", - "license": "MIT", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "optional": true, "peer": true, "dependencies": { @@ -2969,13 +2980,15 @@ }, "node_modules/@httptoolkit/websocket-stream/node_modules/safe-buffer": { "version": "5.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "optional": true, "peer": true }, "node_modules/@httptoolkit/websocket-stream/node_modules/string_decoder": { "version": "1.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "peer": true, "dependencies": { @@ -3910,6 +3923,8 @@ }, "node_modules/@lumieducation/h5p-server": { "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@lumieducation/h5p-server/-/h5p-server-9.2.0.tgz", + "integrity": "sha512-npW5hXyFikFS7LakT6O+4FQgJNHEAyEMRm9VTifyZcNuQ+lMWoz2gGbEuoT4PcTyaK+a1f6G8V8G3882fL0qKQ==", "license": "GPL-3.0-or-later", "dependencies": { "ajv": "^8.11.0", @@ -3940,6 +3955,8 @@ }, "node_modules/@lumieducation/h5p-server/node_modules/axios": { "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.14.9", @@ -3948,6 +3965,8 @@ }, "node_modules/@lumieducation/h5p-server/node_modules/cache-manager": { "version": "3.6.3", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.3.tgz", + "integrity": "sha512-dS4DnV6c6cQcVH5OxzIU1XZaACXwvVIiUPkFytnRmLOACuBGv3GQgRQ1RJGRRw4/9DF14ZK2RFlZu1TUgDniMg==", "license": "MIT", "dependencies": { "async": "3.2.3", @@ -3970,6 +3989,20 @@ } } }, + "node_modules/@lumieducation/h5p-server/node_modules/qs": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", + "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@mikro-orm/cli": { "version": "5.6.16", "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-5.6.16.tgz", @@ -4865,14 +4898,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.2.4", - "license": "MIT", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", + "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.2", + "express": "4.19.2", "multer": "1.4.4-lts.1", - "tslib": "2.6.2" + "tslib": "2.6.3" }, "funding": { "type": "opencollective", @@ -4883,6 +4917,11 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-express/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, "node_modules/@nestjs/platform-socket.io": { "version": "10.3.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", @@ -4902,11 +4941,12 @@ } }, "node_modules/@nestjs/platform-ws": { - "version": "10.3.1", - "license": "MIT", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.3.10.tgz", + "integrity": "sha512-xHiMu162ycuiYJFuIlemCV6CK93Q8eh0Ljvq3sGZ+Oin1Xw7wA67NMADnaEr8Uv/LCUyo813uHNIeQaxL8GkRw==", "dependencies": { - "tslib": "2.6.2", - "ws": "8.16.0" + "tslib": "2.6.3", + "ws": "8.17.1" }, "funding": { "type": "opencollective", @@ -4918,25 +4958,10 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/platform-ws/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } + "node_modules/@nestjs/platform-ws/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/@nestjs/schematics": { "version": "10.0.2", @@ -5488,23 +5513,117 @@ "@redis/client": "^1.0.0" } }, + "node_modules/@rushstack/node-core-library": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.5.0.tgz", + "integrity": "sha512-Cl3MYQ74Je5Y/EngMxcA3SpHjGZ/022nKbAO1aycGfQ+7eKyNCBu0oywj5B1f367GCzuHBgy+3BlVLKysHkXZw==", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.13.1.tgz", + "integrity": "sha512-RfJcpEYfCzEM/8dgRm4xVs8g4x+AdGdZZGa+XmZRWEKbKkVJSHxKmoe5z0f8gFNip0bnlxNavB9cxNaTSY/JRQ==", + "dependencies": { + "@rushstack/node-core-library": "5.5.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@rushstack/ts-command-line": { - "version": "4.17.1", - "license": "MIT", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.1.tgz", + "integrity": "sha512-wU/igKNFRPmQvxiRAM9lEx/5xcFRK72zBp+fbykPKIm83bOmVE0WWQ+ZhX/pcJJqQiodcr0DDzOMw4O8SwpMSQ==", "dependencies": { + "@rushstack/terminal": "0.13.1", "@types/argparse": "1.0.38", "argparse": "~1.0.9", - "colors": "~1.2.1", "string-argv": "~0.3.1" } }, - "node_modules/@rushstack/ts-command-line/node_modules/colors": { - "version": "1.2.5", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@servie/events": { "version": "1.0.0", "license": "MIT" @@ -5647,7 +5766,8 @@ }, "node_modules/@types/argparse": { "version": "1.0.38", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==" }, "node_modules/@types/babel__core": { "version": "7.1.18", @@ -5718,20 +5838,12 @@ "license": "MIT" }, "node_modules/@types/clamscan": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "axios": "^0.24.0" - } - }, - "node_modules/@types/clamscan/node_modules/axios": { - "version": "0.24.0", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.8.tgz", + "integrity": "sha512-HaOKUH+MKgGZAYakboOSHcHga1jGRgD4kpUUslceKtsOqDY16yCLHcURETSF7jOokJOR/Z0k2wk0RL+pN0cbUg==", "dev": true, - "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.4" + "@types/node": "*" } }, "node_modules/@types/compression": { @@ -6102,8 +6214,9 @@ } }, "node_modules/@types/tough-cookie": { - "version": "2.3.8", - "license": "MIT" + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" }, "node_modules/@types/uuid": { "version": "8.3.4", @@ -6754,6 +6867,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-formats": { "version": "2.1.1", "license": "MIT", @@ -6816,12 +6942,6 @@ "node": ">=10" } }, - "node_modules/ansi": { - "version": "0.3.1", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/ansi-colors": { "version": "4.1.1", "dev": true, @@ -6899,16 +7019,6 @@ "dev": true, "license": "MIT" }, - "node_modules/are-we-there-yet": { - "version": "1.0.6", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.0 || ^1.1.13" - } - }, "node_modules/arg": { "version": "5.0.1", "license": "MIT" @@ -7252,51 +7362,67 @@ } }, "node_modules/aws-crt": { - "version": "1.10.6", + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/aws-crt/-/aws-crt-1.21.3.tgz", + "integrity": "sha512-oaiP5zoPkXwbM9T3nwSgq6CBZWx0501iefLPg12FODniIgqGMyzbMXHYC+fxbCoP5SOQVmCwtAfbNuIG5bFENg==", "hasInstallScript": true, - "license": "Apache-2.0", "optional": true, "peer": true, "dependencies": { - "@httptoolkit/websocket-stream": "^6.0.0", - "axios": "^0.24.0", - "cmake-js": "6.3.0", - "crypto-js": "^4.0.0", - "fastestsmallesttextencoderdecoder": "^1.0.22", - "mqtt": "^4.3.4", - "tar": "^6.1.11", - "ws": "^7.5.5" + "@aws-sdk/util-utf8-browser": "^3.109.0", + "@httptoolkit/websocket-stream": "^6.0.1", + "axios": "^1.6.8", + "buffer": "^6.0.3", + "crypto-js": "^4.2.0", + "mqtt": "^4.3.8", + "process": "^0.11.10" } }, - "node_modules/aws-crt/node_modules/axios": { - "version": "0.24.0", - "license": "MIT", + "node_modules/aws-crt/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "optional": true, "peer": true, "dependencies": { - "follow-redirects": "^1.14.4" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/aws-crt/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true + "node_modules/aws-crt/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "utf-8-validate": { - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ], + "optional": true, + "peer": true }, "node_modules/aws-sdk": { "version": "2.1375.0", @@ -7518,25 +7644,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/big-integer": { - "version": "1.6.51", - "license": "Unlicense", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "dev": true, @@ -7615,20 +7722,6 @@ "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "license": "MIT" @@ -7656,13 +7749,6 @@ "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -7680,10 +7766,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "license": "MIT", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -7840,33 +7927,10 @@ "version": "1.1.2", "license": "MIT" }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/buffer-more-ints": { "version": "1.0.0", "license": "MIT" }, - "node_modules/buffer-shims": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/buffers": { - "version": "0.1.1", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/build-url": { "version": "1.3.3", "license": "MIT" @@ -7977,12 +8041,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8081,21 +8151,6 @@ "@types/node": "*" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "license": "MIT/X11", - "optional": true, - "peer": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - } - }, - "node_modules/chainsaw/node_modules/traverse": { - "version": "0.3.9", - "license": "MIT/X11", - "optional": true, - "peer": true - }, "node_modules/chalk": { "version": "5.0.0", "license": "MIT", @@ -8305,15 +8360,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "2.0.0", - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/chrome-trace-event": { "version": "1.0.3", "dev": true, @@ -8415,77 +8461,6 @@ "node": ">=4.2.0" } }, - "node_modules/cliui": { - "version": "3.2.0", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "2.1.1", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "1.0.2", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "3.0.1", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "2.1.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clone": { "version": "1.0.4", "license": "MIT", @@ -8500,152 +8475,6 @@ "node": ">=0.10.0" } }, - "node_modules/cmake-js": { - "version": "6.3.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "axios": "^0.21.1", - "debug": "^4", - "fs-extra": "^5.0.0", - "is-iojs": "^1.0.1", - "lodash": "^4", - "memory-stream": "0", - "npmlog": "^1.2.0", - "rc": "^1.2.7", - "semver": "^5.0.3", - "splitargs": "0", - "tar": "^4", - "unzipper": "^0.8.13", - "url-join": "0", - "which": "^1.0.9", - "yargs": "^3.6.0" - }, - "bin": { - "cmake-js": "bin/cmake-js" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cmake-js/node_modules/axios": { - "version": "0.21.4", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/cmake-js/node_modules/chownr": { - "version": "1.1.4", - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/cmake-js/node_modules/fs-extra": { - "version": "5.0.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/cmake-js/node_modules/fs-minipass": { - "version": "1.2.7", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/cmake-js/node_modules/jsonfile": { - "version": "4.0.0", - "license": "MIT", - "optional": true, - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/cmake-js/node_modules/minipass": { - "version": "2.9.0", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/cmake-js/node_modules/minizlib": { - "version": "1.3.3", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/cmake-js/node_modules/mkdirp": { - "version": "0.5.5", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/cmake-js/node_modules/semver": { - "version": "5.7.2", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/cmake-js/node_modules/tar": { - "version": "4.4.19", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" - } - }, - "node_modules/cmake-js/node_modules/universalify": { - "version": "0.1.2", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/cmake-js/node_modules/yallist": { - "version": "3.1.1", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/co": { "version": "4.6.0", "dev": true, @@ -8655,15 +8484,6 @@ "node": ">= 0.12.0" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "dev": true, @@ -9041,8 +8861,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.5.0", - "license": "MIT", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -9434,15 +9255,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "license": "MIT" @@ -9474,15 +9286,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "license": "MIT", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -9515,12 +9331,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/depd": { "version": "1.1.2", "license": "MIT", @@ -9721,7 +9531,8 @@ }, "node_modules/duplexify": { "version": "3.7.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "optional": true, "peer": true, "dependencies": { @@ -9732,8 +9543,9 @@ } }, "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.7", - "license": "MIT", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "optional": true, "peer": true, "dependencies": { @@ -9748,13 +9560,15 @@ }, "node_modules/duplexify/node_modules/safe-buffer": { "version": "5.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "optional": true, "peer": true }, "node_modules/duplexify/node_modules/string_decoder": { "version": "1.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "peer": true, "dependencies": { @@ -9830,9 +9644,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", - "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -9843,46 +9657,25 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", @@ -9899,26 +9692,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "dev": true, @@ -10008,6 +9781,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.3.0", "dev": true, @@ -10049,12 +9841,14 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, - "license": "ISC", "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -10999,6 +10793,25 @@ "node": ">=6" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" + }, "node_modules/espree": { "version": "9.4.1", "dev": true, @@ -11156,15 +10969,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "license": "MIT", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -11340,28 +11154,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -11371,7 +11163,8 @@ }, "node_modules/express/node_modules/depd": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "engines": { "node": ">= 0.8" } @@ -11392,20 +11185,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/http-errors": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "license": "MIT" @@ -11424,13 +11203,6 @@ "version": "0.1.7", "license": "MIT" }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/ext": { "version": "1.7.0", "license": "ISC", @@ -11527,12 +11299,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "license": "CC0-1.0", - "optional": true, - "peer": true - }, "node_modules/fastq": { "version": "1.13.0", "license": "ISC", @@ -11691,8 +11457,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "license": "MIT", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -11961,7 +11728,8 @@ }, "node_modules/fs-jetpack": { "version": "4.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-4.3.1.tgz", + "integrity": "sha512-dbeOK84F6BiQzk2yqqCVwCPWTxAvVGJ3fMQc6E2wuEohS28mR6yHngbrKuVCK1KHRx/ccByDylqu4H5PCP2urQ==", "dependencies": { "minimatch": "^3.0.2", "rimraf": "^2.6.3" @@ -11969,7 +11737,9 @@ }, "node_modules/fs-jetpack/node_modules/rimraf": { "version": "2.7.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -11977,18 +11747,6 @@ "rimraf": "bin.js" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs-monkey": { "version": "1.0.3", "dev": true, @@ -11998,43 +11756,18 @@ "version": "1.0.0", "license": "ISC" }, - "node_modules/fstream": { - "version": "1.0.12", - "license": "ISC", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/mkdirp": { - "version": "0.5.5", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { @@ -12072,19 +11805,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "1.2.7", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "ansi": "^0.3.0", - "has-unicode": "^2.0.0", - "lodash.pad": "^4.1.0", - "lodash.padend": "^4.1.0", - "lodash.padstart": "^4.1.0" - } - }, "node_modules/generic-pool": { "version": "3.9.0", "license": "MIT", @@ -12119,22 +11839,28 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "license": "MIT", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12435,10 +12161,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12477,12 +12204,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/hasha": { "version": "5.2.2", "dev": true, @@ -12602,6 +12323,29 @@ "entities": "^2.0.0" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "license": "MIT", @@ -12749,6 +12493,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.1.0", "dev": true, @@ -12795,12 +12547,6 @@ "version": "2.0.4", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/inquirer": { "version": "7.3.3", "dev": true, @@ -12916,15 +12662,6 @@ "node": ">= 0.10" } }, - "node_modules/invert-kv": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ioredis": { "version": "5.3.2", "license": "MIT", @@ -12969,12 +12706,31 @@ "node": ">=0.10" } }, - "node_modules/ip": { - "version": "2.0.0", - "license": "MIT" + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/ip-regex": { "version": "2.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -13174,12 +12930,6 @@ "node": ">=8" } }, - "node_modules/is-iojs": { - "version": "1.1.0", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/is-ip": { "version": "2.0.0", "dev": true, @@ -13203,7 +12953,8 @@ }, "node_modules/is-number": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { "node": ">=0.12.0" } @@ -13376,7 +13127,8 @@ }, "node_modules/isomorphic-ws": { "version": "4.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", "optional": true, "peer": true, "peerDependencies": { @@ -15355,7 +15107,6 @@ }, "node_modules/jju": { "version": "1.4.0", - "dev": true, "license": "MIT" }, "node_modules/jmespath": { @@ -15703,8 +15454,9 @@ } }, "node_modules/jwks-rsa/node_modules/jose": { - "version": "2.0.6", - "license": "MIT", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.7.tgz", + "integrity": "sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==", "dependencies": { "@panva/asn1.js": "^1.0.0" }, @@ -15861,18 +15613,6 @@ "language-subtag-registry": "~0.3.2" } }, - "node_modules/lcid": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ldap-filter": { "version": "0.2.2", "license": "MIT", @@ -15975,12 +15715,6 @@ "dev": true, "license": "MIT" }, - "node_modules/listenercount": { - "version": "1.0.1", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/load-tsconfig": { "version": "0.2.3", "dev": true, @@ -16075,29 +15809,6 @@ "version": "4.1.1", "license": "MIT" }, - "node_modules/lodash.pad": { - "version": "4.5.1", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/lodash.padend": { - "version": "4.6.1", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/lodash.padstart": { - "version": "4.6.1", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/lodash.set": { - "version": "4.3.2", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.sortby": { "version": "4.7.0", "dev": true, @@ -16412,33 +16123,6 @@ "license": "MIT", "optional": true }, - "node_modules/memory-stream": { - "version": "0.0.3", - "license": "MMIT", - "optional": true, - "peer": true, - "dependencies": { - "readable-stream": "~1.0.26-2" - } - }, - "node_modules/memory-stream/node_modules/isarray": { - "version": "0.0.1", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/memory-stream/node_modules/readable-stream": { - "version": "1.0.34", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "node_modules/merge": { "version": "2.1.1", "license": "MIT" @@ -16548,38 +16232,13 @@ "version": "1.2.6", "license": "MIT" }, - "node_modules/minipass": { - "version": "3.1.6", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/mixwith": { "version": "0.1.1", "license": "Apache-2.0" }, "node_modules/mkdirp": { "version": "1.0.4", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -17051,8 +16710,9 @@ } }, "node_modules/mqtt": { - "version": "4.3.5", - "license": "MIT", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz", + "integrity": "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==", "optional": true, "peer": true, "dependencies": { @@ -17188,8 +16848,9 @@ } }, "node_modules/mqtt/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "optional": true, "peer": true, "engines": { @@ -17363,14 +17024,15 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.6", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -17500,13 +17162,13 @@ } }, "node_modules/nock": { - "version": "13.2.4", + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash.set": "^4.3.2", "propagate": "^2.0.0" }, "engines": { @@ -17702,17 +17364,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "1.2.1", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "ansi": "~0.3.0", - "are-we-there-yet": "~1.0.0", - "gauge": "~1.2.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "license": "BSD-2-Clause", @@ -17733,15 +17384,6 @@ "js-sdsl": "^2.1.2" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nyc": { "version": "15.1.0", "dev": true, @@ -18251,18 +17893,6 @@ "node": ">=8" } }, - "node_modules/os-locale": { - "version": "1.4.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/os-name": { "version": "4.0.1", "dev": true, @@ -18641,8 +18271,9 @@ "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" }, "node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -18691,8 +18322,9 @@ "version": "1.0.0" }, "node_modules/pony-cause": { - "version": "2.1.10", - "license": "0BSD", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", "engines": { "node": ">=12.0.0" } @@ -18719,28 +18351,20 @@ } }, "node_modules/popsicle-cookie-jar": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/popsicle-cookie-jar/-/popsicle-cookie-jar-1.0.1.tgz", + "integrity": "sha512-QVIZhADP8nDbXIQW6wq8GU9IOSE8INUACO/9KD9TFKQ7qq8r/y3qUDz59xIi6p6TH19lCJJyBAPSXP1liIoySw==", "dependencies": { - "@types/tough-cookie": "^2.3.5", - "tough-cookie": "^3.0.1" + "@types/tough-cookie": "^4.0.2", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=8" }, "peerDependencies": { "servie": "^4.0.0" } }, - "node_modules/popsicle-cookie-jar/node_modules/tough-cookie": { - "version": "3.0.1", - "license": "BSD-3-Clause", - "dependencies": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/popsicle-redirects": { "version": "1.1.0", "license": "MIT", @@ -18773,7 +18397,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "funding": [ { "type": "opencollective", @@ -18782,13 +18408,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -19265,6 +18894,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "license": "MIT" @@ -19476,71 +19115,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.1", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/depd": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "peer": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "dev": true, @@ -20643,7 +20217,8 @@ }, "node_modules/saslprep": { "version": "1.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" @@ -20777,20 +20352,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/http-errors": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -20805,13 +20366,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/serialize-javascript": { "version": "6.0.0", "dev": true, @@ -20875,13 +20429,16 @@ "license": "ISC" }, "node_modules/set-function-length": { - "version": "1.1.1", - "license": "MIT", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -20899,12 +20456,6 @@ "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" @@ -20973,12 +20524,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "license": "MIT", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21016,9 +20572,10 @@ "license": "MIT" }, "node_modules/simple-update-notifier": { - "version": "1.0.7", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", "dev": true, - "license": "MIT", "dependencies": { "semver": "~7.0.0" }, @@ -21155,12 +20712,12 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", - "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dependencies": { "debug": "~4.3.4", - "ws": "~8.11.0" + "ws": "~8.17.1" } }, "node_modules/socket.io-adapter/node_modules/debug": { @@ -21179,26 +20736,6 @@ } } }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", @@ -21231,14 +20768,15 @@ "license": "X11" }, "node_modules/socks": { - "version": "2.7.1", - "license": "MIT", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -21250,8 +20788,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -21342,12 +20881,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/splitargs": { - "version": "0.0.7", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/sprintf-js": { "version": "1.0.3", "license": "BSD-3-Clause" @@ -21510,6 +21043,14 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stealthy-require": { "version": "1.1.1", "license": "ISC", @@ -21581,7 +21122,8 @@ }, "node_modules/string-argv": { "version": "0.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "engines": { "node": ">=0.6.19" } @@ -21986,23 +21528,6 @@ "node": ">=6" } }, - "node_modules/tar": { - "version": "6.1.11", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/tarn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", @@ -22252,7 +21777,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": { "is-number": "^7.0.0" }, @@ -22968,7 +22494,8 @@ }, "node_modules/umzug": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.2.1.tgz", + "integrity": "sha512-XyWQowvP9CKZycKc/Zg9SYWrAWX/gJCE799AUTFqk8yC3tp44K1xWr3LoFF0MNEjClKOo1suCr5ASnoy+KltdA==", "dependencies": { "@rushstack/ts-command-line": "^4.12.2", "emittery": "^0.12.1", @@ -22983,14 +22510,16 @@ }, "node_modules/umzug/node_modules/brace-expansion": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/umzug/node_modules/emittery": { "version": "0.12.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.12.1.tgz", + "integrity": "sha512-pYyW59MIZo0HxPFf+Vb3+gacUu0gxVS3TZwB2ClwkEZywgF9f9OJDoVmNLojTn0vKX3tO9LC+pdQEcLP4Oz/bQ==", "engines": { "node": ">=12" }, @@ -23000,7 +22529,9 @@ }, "node_modules/umzug/node_modules/glob": { "version": "8.1.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -23017,7 +22548,8 @@ }, "node_modules/umzug/node_modules/minimatch": { "version": "5.1.6", - "license": "ISC", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -23027,7 +22559,8 @@ }, "node_modules/umzug/node_modules/type-fest": { "version": "2.19.0", - "license": "(MIT OR CC0-1.0)", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "engines": { "node": ">=12.20" }, @@ -23058,10 +22591,11 @@ "license": "MIT" }, "node_modules/undici": { - "version": "5.25.2", - "license": "MIT", + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "dependencies": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" }, "engines": { "node": ">=14.0" @@ -23116,50 +22650,6 @@ "node": ">=8" } }, - "node_modules/unzipper": { - "version": "0.8.14", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "~1.0.10", - "listenercount": "~1.0.1", - "readable-stream": "~2.1.5", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/bluebird": { - "version": "3.4.7", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/unzipper/node_modules/process-nextick-args": { - "version": "1.0.7", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/unzipper/node_modules/readable-stream": { - "version": "2.1.5", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-shims": "^1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, "node_modules/upath": { "version": "2.0.1", "license": "MIT", @@ -23183,12 +22673,6 @@ "querystring": "0.2.0" } }, - "node_modules/url-join": { - "version": "0.0.1", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/url-parse": { "version": "1.5.10", "license": "MIT", @@ -23574,18 +23058,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/window-size": { - "version": "0.1.4", - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "window-size": "cli.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/windows-release": { "version": "4.0.0", "dev": true, @@ -23918,12 +23390,6 @@ "yjs": "^13.0.0" } }, - "node_modules/y18n": { - "version": "3.2.2", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/yaassertion": { "version": "1.0.2", "license": "MIT" @@ -23940,21 +23406,6 @@ "node": ">= 6" } }, - "node_modules/yargs": { - "version": "3.32.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "camelcase": "^2.0.1", - "cliui": "^3.0.3", - "decamelize": "^1.1.1", - "os-locale": "^1.4.0", - "string-width": "^1.0.1", - "window-size": "^0.1.4", - "y18n": "^3.2.0" - } - }, "node_modules/yargs-parser": { "version": "20.2.4", "license": "ISC", @@ -23998,62 +23449,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "2.1.1", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/camelcase": { - "version": "2.1.1", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "1.0.2", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "3.0.1", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/yauzl": { "version": "2.10.0", "license": "MIT", From 8a11bc9e2b20df4efa8b6a0d3c41e1fea9ea60f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:39:19 +0200 Subject: [PATCH 35/35] N21-2057 Team invitations with duplicate emails (#5114) --- .../email-already-exists.loggable.spec.ts | 32 ----- .../loggable/email-already-exists.loggable.ts | 15 --- .../modules/provisioning/loggable/index.ts | 1 - ...ulconnex-user-provisioning.service.spec.ts | 49 -------- .../schulconnex-user-provisioning.service.ts | 37 +----- .../modules/user/service/user.service.spec.ts | 115 ------------------ .../src/modules/user/service/user.service.ts | 14 --- .../src/shared/domain/entity/user.entity.ts | 1 - src/services/user/hooks/publicTeachers.js | 2 +- 9 files changed, 7 insertions(+), 259 deletions(-) delete mode 100644 apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.spec.ts delete mode 100644 apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.ts diff --git a/apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.spec.ts deleted file mode 100644 index 87833c6967c..00000000000 --- a/apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { EmailAlreadyExistsLoggable } from '@modules/provisioning/loggable/email-already-exists.loggable'; - -describe('EmailAlreadyExistsLoggableException', () => { - describe('getLogMessage', () => { - const setup = () => { - const email = 'mock-email'; - const externalId = '789'; - - const loggable = new EmailAlreadyExistsLoggable(email, externalId); - - return { - loggable, - email, - externalId, - }; - }; - - it('should return the correct log message', () => { - const { loggable, email, externalId } = setup(); - - const message = loggable.getLogMessage(); - - expect(message).toEqual({ - message: 'The Email to be provisioned already exists.', - data: { - email, - externalId, - }, - }); - }); - }); -}); diff --git a/apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.ts b/apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.ts deleted file mode 100644 index 1e4cd7ed77d..00000000000 --- a/apps/server/src/modules/provisioning/loggable/email-already-exists.loggable.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; - -export class EmailAlreadyExistsLoggable implements Loggable { - constructor(private readonly email: string, private readonly externalId?: string) {} - - getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { - return { - message: 'The Email to be provisioned already exists.', - data: { - email: this.email, - externalId: this.externalId, - }, - }; - } -} diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts index 48efe5c4f67..89b4baf9b7a 100644 --- a/apps/server/src/modules/provisioning/loggable/index.ts +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -1,5 +1,4 @@ export * from './user-for-group-not-found.loggable'; export * from './school-for-group-not-found.loggable'; export * from './group-role-unknown.loggable'; -export { EmailAlreadyExistsLoggable } from './email-already-exists.loggable'; export { SchoolExternalToolCreatedLoggable } from './school-external-tool-created.loggable'; diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts index 08f3f70bb38..05e6fa3d3ad 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts @@ -22,7 +22,6 @@ describe(SchulconnexUserProvisioningService.name, () => { let userService: DeepMocked; let roleService: DeepMocked; let accountService: DeepMocked; - let logger: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -51,7 +50,6 @@ describe(SchulconnexUserProvisioningService.name, () => { userService = module.get(UserService); roleService = module.get(RoleService); accountService = module.get(AccountService); - logger = module.get(Logger); }); afterAll(async () => { @@ -140,27 +138,6 @@ describe(SchulconnexUserProvisioningService.name, () => { }); }); - it('should call user service to check uniqueness of email', async () => { - const { externalUser, schoolId, systemId } = setupUser(); - - userService.findByExternalId.mockResolvedValue(null); - - await service.provisionExternalUser(externalUser, systemId, schoolId); - - expect(userService.isEmailUniqueForExternal).toHaveBeenCalledWith(externalUser.email, undefined); - }); - - it('should call the user service to save the user', async () => { - const { externalUser, schoolId, savedUser, systemId } = setupUser(); - - userService.findByExternalId.mockResolvedValue(null); - userService.isEmailUniqueForExternal.mockResolvedValue(true); - - await service.provisionExternalUser(externalUser, systemId, schoolId); - - expect(userService.save).toHaveBeenCalledWith(new UserDO({ ...savedUser, id: undefined })); - }); - it('should return the saved user', async () => { const { externalUser, schoolId, savedUser, systemId } = setupUser(); @@ -198,35 +175,9 @@ describe(SchulconnexUserProvisioningService.name, () => { await expect(promise).rejects.toThrow(UnprocessableEntityException); }); }); - - describe('when the external user has an email, that already exists', () => { - it('should log EmailAlreadyExistsLoggable', async () => { - const { externalUser, systemId, schoolId } = setupUser(); - - userService.findByExternalId.mockResolvedValue(null); - userService.isEmailUniqueForExternal.mockResolvedValue(false); - - await service.provisionExternalUser(externalUser, systemId, schoolId); - - expect(logger.warning).toHaveBeenCalledWith({ - email: externalUser.email, - }); - }); - }); }); describe('when the user already exists', () => { - it('should call user service to check uniqueness of email', async () => { - const { externalUser, schoolId, systemId, existingUser } = setupUser(); - - userService.findByExternalId.mockResolvedValue(existingUser); - userService.isEmailUniqueForExternal.mockResolvedValue(true); - - await service.provisionExternalUser(externalUser, systemId, schoolId); - - expect(userService.isEmailUniqueForExternal).toHaveBeenCalledWith(externalUser.email, existingUser.externalId); - }); - it('should call the user service to save the user', async () => { const { externalUser, schoolId, existingUser, systemId } = setupUser(); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts index f451486e0a7..783076572de 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts @@ -1,12 +1,10 @@ import { AccountSave, AccountService } from '@modules/account'; -import { EmailAlreadyExistsLoggable } from '@modules/provisioning/loggable'; import { RoleDto, RoleService } from '@modules/role'; import { UserService } from '@modules/user'; import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { RoleReference, UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { Logger } from '@src/core/logger'; import CryptoJS from 'crypto-js'; import { ExternalUserDto } from '../../../dto'; @@ -15,8 +13,7 @@ export class SchulconnexUserProvisioningService { constructor( private readonly userService: UserService, private readonly roleService: RoleService, - private readonly accountService: AccountService, - private readonly logger: Logger + private readonly accountService: AccountService ) {} public async provisionExternalUser( @@ -26,14 +23,12 @@ export class SchulconnexUserProvisioningService { ): Promise { const foundUser: UserDO | null = await this.userService.findByExternalId(externalUser.externalId, systemId); - const isEmailUnique: boolean = await this.checkUniqueEmail(externalUser.email, foundUser?.externalId); - const roleRefs: RoleReference[] | undefined = await this.createRoleReferences(externalUser.roles); let createNewAccount = false; let user: UserDO; if (foundUser) { - user = this.updateUser(externalUser, foundUser, isEmailUnique, roleRefs, schoolId); + user = this.updateUser(externalUser, foundUser, roleRefs, schoolId); } else { if (!schoolId) { throw new UnprocessableEntityException( @@ -42,7 +37,7 @@ export class SchulconnexUserProvisioningService { } createNewAccount = true; - user = this.createUser(externalUser, isEmailUnique, schoolId, roleRefs); + user = this.createUser(externalUser, schoolId, roleRefs); } const savedUser: UserDO = await this.userService.save(user); @@ -59,20 +54,6 @@ export class SchulconnexUserProvisioningService { return savedUser; } - private async checkUniqueEmail(email?: string, externalId?: string): Promise { - if (email) { - const isEmailUnique: boolean = await this.userService.isEmailUniqueForExternal(email, externalId); - - if (!isEmailUnique) { - this.logger.warning(new EmailAlreadyExistsLoggable(email, externalId)); - } - - return isEmailUnique; - } - - return true; - } - private async createRoleReferences(roles?: RoleName[]): Promise { if (roles) { const foundRoles: RoleDto[] = await this.roleService.findByNames(roles); @@ -89,14 +70,13 @@ export class SchulconnexUserProvisioningService { private updateUser( externalUser: ExternalUserDto, foundUser: UserDO, - isEmailUnique: boolean, roleRefs?: RoleReference[], schoolId?: string ): UserDO { const user: UserDO = foundUser; user.firstName = externalUser.firstName ?? foundUser.firstName; user.lastName = externalUser.lastName ?? foundUser.lastName; - user.email = isEmailUnique ? externalUser.email ?? foundUser.email : foundUser.email; + user.email = externalUser.email ?? foundUser.email; user.roles = roleRefs ?? foundUser.roles; user.schoolId = schoolId ?? foundUser.schoolId; user.birthday = externalUser.birthday ?? foundUser.birthday; @@ -104,17 +84,12 @@ export class SchulconnexUserProvisioningService { return user; } - private createUser( - externalUser: ExternalUserDto, - isEmailUnique: boolean, - schoolId: string, - roleRefs?: RoleReference[] - ): UserDO { + private createUser(externalUser: ExternalUserDto, schoolId: string, roleRefs?: RoleReference[]): UserDO { const user: UserDO = new UserDO({ externalId: externalUser.externalId, firstName: externalUser.firstName ?? '', lastName: externalUser.lastName ?? '', - email: isEmailUnique ? externalUser.email ?? '' : '', + email: externalUser.email ?? '', roles: roleRefs ?? [], schoolId, birthday: externalUser.birthday, 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 98bd755ef61..eee9f447616 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -946,119 +946,4 @@ describe('UserService', () => { }); }); }); - - describe('isEmailUniqueForExternal', () => { - describe('when email does not exist', () => { - const setup = () => { - const email = 'email'; - - userDORepo.findByEmail.mockResolvedValue([]); - - return { - email, - }; - }; - - it('should return true', async () => { - const { email } = setup(); - - const result: boolean = await service.isEmailUniqueForExternal(email, undefined); - - expect(result).toBe(true); - }); - }); - - describe('when an existing user is found', () => { - describe('when existing user is the external user', () => { - const setup = () => { - const email = 'email'; - const externalId = 'externalId'; - const existingUser: UserDO = userDoFactory.build({ email, externalId }); - - userDORepo.findByEmail.mockResolvedValue([existingUser]); - - return { - email, - externalId, - }; - }; - - it('should return true', async () => { - const { email, externalId } = setup(); - - const result: boolean = await service.isEmailUniqueForExternal(email, externalId); - - expect(result).toBe(true); - }); - }); - - describe('when existing user is not the external user', () => { - const setup = () => { - const email = 'email'; - const externalId = 'externalId'; - const otherUserWithSameEmail: UserDO = userDoFactory.build({ email }); - - userDORepo.findByEmail.mockResolvedValue([otherUserWithSameEmail]); - - return { - email, - externalId, - }; - }; - - it('should return false', async () => { - const { email, externalId } = setup(); - - const result: boolean = await service.isEmailUniqueForExternal(email, externalId); - - expect(result).toBe(false); - }); - }); - - describe('when existing user is not the external user and external user is not already provisioned.', () => { - const setup = () => { - const email = 'email'; - const otherUserWithSameEmail: UserDO = userDoFactory.build({ email }); - - userDORepo.findByEmail.mockResolvedValue([otherUserWithSameEmail]); - - return { - email, - }; - }; - - it('should return false', async () => { - const { email } = setup(); - - const result: boolean = await service.isEmailUniqueForExternal(email, undefined); - - expect(result).toBe(false); - }); - }); - }); - - describe('when multiple users are found', () => { - const setup = () => { - const email = 'email'; - const externalId = 'externalId'; - const existingUser: UserDO = userDoFactory.build({ email, externalId }); - const otherUserWithSameEmail: UserDO = userDoFactory.build({ email }); - - userDORepo.findByEmail.mockResolvedValue([existingUser, otherUserWithSameEmail]); - - return { - email, - externalId, - }; - }; - - it('should return false', async () => { - const { email, externalId } = setup(); - - const result: boolean = await service.isEmailUniqueForExternal(email, externalId); - - expect(result).toBe(false); - }); - }); - }); }); diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index f474bdfbefc..33394ff30dc 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -283,18 +283,4 @@ export class UserService implements DeletionService, IEventHandler { - const foundUsers: UserDO[] = await this.findByEmail(email); - if (!externalId && foundUsers.length) { - return false; - } - - const unmatchedUsers: UserDO[] = foundUsers.filter((user: UserDO) => user.externalId !== externalId); - if (unmatchedUsers.length) { - return false; - } - - return true; - } } diff --git a/apps/server/src/shared/domain/entity/user.entity.ts b/apps/server/src/shared/domain/entity/user.entity.ts index a253766e29a..cfb27773c5b 100644 --- a/apps/server/src/shared/domain/entity/user.entity.ts +++ b/apps/server/src/shared/domain/entity/user.entity.ts @@ -51,7 +51,6 @@ interface UserInfo { export class User extends BaseEntityWithTimestamps implements EntityWithSchool { @Property() @Index() - // @Unique() email: string; @Property() diff --git a/src/services/user/hooks/publicTeachers.js b/src/services/user/hooks/publicTeachers.js index c44e7eba5cf..9a59e3a6fdd 100644 --- a/src/services/user/hooks/publicTeachers.js +++ b/src/services/user/hooks/publicTeachers.js @@ -26,7 +26,7 @@ const mapRoleFilterQuery = (hook) => { const filterForPublicTeacher = (hook) => { // Limit accessible fields - hook.params.query.$select = ['_id', 'firstName', 'lastName']; + hook.params.query.$select = ['_id', 'firstName', 'lastName', 'schoolId']; // Limit accessible user (only teacher which are discoverable) hook.params.query.roles = ['teacher'];