From 0333c9c543e7168873a2a3f673535e23dca98f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Thu, 15 Aug 2024 15:14:31 +0200 Subject: [PATCH 1/8] add deleted element to board --- .../board/controller/card.controller.ts | 6 +- .../controller/dto/card/card.response.ts | 5 +- .../element/any-content-element.response.ts | 4 +- .../dto/element/deleted-element.response.ts | 37 +++++++++ .../board/controller/dto/element/index.ts | 1 + .../content-element-response.factory.spec.ts | 14 +++- .../content-element-response.factory.ts | 4 +- .../mapper/deleted-element-response.mapper.ts | 30 ++++++++ .../modules/board/controller/mapper/index.ts | 1 + .../board/domain/board-node.factory.ts | 4 +- .../board/domain/deleted-element.do.ts | 23 ++++++ apps/server/src/modules/board/domain/index.ts | 1 + .../media-board/types/any-media-board-node.ts | 10 --- .../src/modules/board/domain/type-mapping.ts | 2 + .../board/domain/types/any-content-element.ts | 7 +- .../board/domain/types/board-node-props.ts | 8 +- .../domain/types/board-node-type.enum.ts | 1 + .../domain/types/content-element-type.enum.ts | 1 + .../board/repo/entity/board-node.entity.ts | 17 +++- .../repo/types/board-node-entity-props.ts | 4 +- .../board/service/board-node.service.ts | 19 +++++ ...rnal-tool-deleted-event-handler.service.ts | 35 +++++++++ .../internal/board-node-copy.service.ts | 20 +++++ .../board/testing/deleted-element.factory.ts | 20 +++++ .../server/src/modules/board/testing/index.ts | 1 + .../modules/copy-helper/types/copy.types.ts | 1 + .../modules/tool/common/common-tool.module.ts | 11 ++- .../service/common-tool-delete.service.ts | 77 +++++++++++++++++++ .../src/modules/tool/common/service/index.ts | 1 + .../context-external-tool-deleted.event.ts | 12 +++ .../domain/event/index.ts | 1 + .../context-external-tool/domain/index.ts | 1 + .../service/context-external-tool.service.ts | 17 +--- .../external-tool/external-tool.module.ts | 2 - .../service/external-tool.service.ts | 18 ++--- .../tool/external-tool/uc/external-tool.uc.ts | 4 +- .../service/school-external-tool.service.ts | 9 ++- .../uc/school-external-tool.uc.spec.ts | 18 ++--- .../uc/school-external-tool.uc.ts | 9 +-- .../school-external-tool.repo.spec.ts | 2 +- .../school-external-tool.repo.ts | 4 +- 41 files changed, 380 insertions(+), 82 deletions(-) create mode 100644 apps/server/src/modules/board/controller/dto/element/deleted-element.response.ts create mode 100644 apps/server/src/modules/board/controller/mapper/deleted-element-response.mapper.ts create mode 100644 apps/server/src/modules/board/domain/deleted-element.do.ts create mode 100644 apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts create mode 100644 apps/server/src/modules/board/testing/deleted-element.factory.ts create mode 100644 apps/server/src/modules/tool/common/service/common-tool-delete.service.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/event/context-external-tool-deleted.event.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/event/index.ts diff --git a/apps/server/src/modules/board/controller/card.controller.ts b/apps/server/src/modules/board/controller/card.controller.ts index 341d1e85dc9..aba2ff2e4dd 100644 --- a/apps/server/src/modules/board/controller/card.controller.ts +++ b/apps/server/src/modules/board/controller/card.controller.ts @@ -22,6 +22,7 @@ import { CardListResponse, CardUrlParams, CreateContentElementBodyParams, + DeletedElementResponse, DrawingElementResponse, ExternalToolElementResponse, FileElementResponse, @@ -121,7 +122,9 @@ export class CardController { FileElementResponse, LinkElementResponse, RichTextElementResponse, - SubmissionContainerElementResponse + SubmissionContainerElementResponse, + DrawingElementResponse, + DeletedElementResponse ) @ApiResponse({ status: 201, @@ -133,6 +136,7 @@ export class CardController { { $ref: getSchemaPath(RichTextElementResponse) }, { $ref: getSchemaPath(SubmissionContainerElementResponse) }, { $ref: getSchemaPath(DrawingElementResponse) }, + { $ref: getSchemaPath(DeletedElementResponse) }, ], }, }) diff --git a/apps/server/src/modules/board/controller/dto/card/card.response.ts b/apps/server/src/modules/board/controller/dto/card/card.response.ts index 31f8049e835..aa641fdd736 100644 --- a/apps/server/src/modules/board/controller/dto/card/card.response.ts +++ b/apps/server/src/modules/board/controller/dto/card/card.response.ts @@ -3,6 +3,7 @@ import { DecodeHtmlEntities } from '@shared/controller'; import { AnyContentElementResponse, CollaborativeTextEditorElementResponse, + DeletedElementResponse, DrawingElementResponse, ExternalToolElementResponse, FileElementResponse, @@ -20,7 +21,8 @@ import { VisibilitySettingsResponse } from './visibility-settings.response'; RichTextElementResponse, DrawingElementResponse, SubmissionContainerElementResponse, - CollaborativeTextEditorElementResponse + CollaborativeTextEditorElementResponse, + DeletedElementResponse ) export class CardResponse { constructor({ id, title, height, elements, visibilitySettings, timestamps }: CardResponse) { @@ -55,6 +57,7 @@ export class CardResponse { { $ref: getSchemaPath(SubmissionContainerElementResponse) }, { $ref: getSchemaPath(DrawingElementResponse) }, { $ref: getSchemaPath(CollaborativeTextEditorElementResponse) }, + { $ref: getSchemaPath(DeletedElementResponse) }, ], }, }) diff --git a/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts b/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts index fa77ed11755..dbe2adc1e01 100644 --- a/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts @@ -1,4 +1,5 @@ import { CollaborativeTextEditorElementResponse } from './collaborative-text-editor-element.response'; +import { DeletedElementResponse } from './deleted-element.response'; import { DrawingElementResponse } from './drawing-element.response'; import { ExternalToolElementResponse } from './external-tool-element.response'; import { FileElementResponse } from './file-element.response'; @@ -13,7 +14,8 @@ export type AnyContentElementResponse = | SubmissionContainerElementResponse | ExternalToolElementResponse | DrawingElementResponse - | CollaborativeTextEditorElementResponse; + | CollaborativeTextEditorElementResponse + | DeletedElementResponse; export const isFileElementResponse = (element: AnyContentElementResponse): element is FileElementResponse => element instanceof FileElementResponse; diff --git a/apps/server/src/modules/board/controller/dto/element/deleted-element.response.ts b/apps/server/src/modules/board/controller/dto/element/deleted-element.response.ts new file mode 100644 index 00000000000..785f02616b8 --- /dev/null +++ b/apps/server/src/modules/board/controller/dto/element/deleted-element.response.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ContentElementType } from '../../../domain'; +import { TimestampsResponse } from '../timestamps.response'; + +export class DeletedElementContent { + constructor(props: DeletedElementContent) { + this.title = props.title; + this.deletedElementType = props.deletedElementType; + } + + @ApiProperty() + title: string; + + @ApiProperty({ enum: ContentElementType, enumName: 'ContentElementType' }) + deletedElementType: ContentElementType; +} + +export class DeletedElementResponse { + constructor(props: DeletedElementResponse) { + this.id = props.id; + this.type = props.type; + this.content = props.content; + this.timestamps = props.timestamps; + } + + @ApiProperty({ pattern: '[a-f0-9]{24}' }) + id: string; + + @ApiProperty({ enum: ContentElementType, enumName: 'ContentElementType' }) + type: ContentElementType.DELETED; + + @ApiProperty() + content: DeletedElementContent; + + @ApiProperty() + timestamps: TimestampsResponse; +} diff --git a/apps/server/src/modules/board/controller/dto/element/index.ts b/apps/server/src/modules/board/controller/dto/element/index.ts index 73c8d08f93c..0a85fb2c699 100644 --- a/apps/server/src/modules/board/controller/dto/element/index.ts +++ b/apps/server/src/modules/board/controller/dto/element/index.ts @@ -8,3 +8,4 @@ export * from './link-element.response'; export * from './rich-text-element.response'; export * from './submission-container-element.response'; export * from './update-element-content.body.params'; +export * from './deleted-element.response'; diff --git a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts index a4bad1ef915..d5c4942a777 100644 --- a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts +++ b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts @@ -1,15 +1,17 @@ import { NotImplementedException } from '@nestjs/common'; import { - fileElementFactory, + deletedElementFactory, drawingElementFactory, + fileElementFactory, linkElementFactory, richTextElementFactory, submissionContainerElementFactory, } from '../../testing'; import { + DeletedElementResponse, + DrawingElementResponse, FileElementResponse, LinkElementResponse, - DrawingElementResponse, RichTextElementResponse, SubmissionContainerElementResponse, } from '../dto'; @@ -55,6 +57,14 @@ describe(ContentElementResponseFactory.name, () => { expect(result).toBeInstanceOf(SubmissionContainerElementResponse); }); + it('should return instance of DeletedElementResponse', () => { + const drawingElement = deletedElementFactory.build(); + + const result = ContentElementResponseFactory.mapToResponse(drawingElement); + + expect(result).toBeInstanceOf(DeletedElementResponse); + }); + it('should throw NotImplementedException', () => { // @ts-expect-error check unknown type expect(() => ContentElementResponseFactory.mapToResponse('UNKNOWN')).toThrow(NotImplementedException); diff --git a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts index 4047a921c98..dec7e12420e 100644 --- a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts +++ b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts @@ -3,12 +3,13 @@ import { AnyBoardNode, FileElement, RichTextElement } from '../../domain'; import { AnyContentElementResponse, FileElementResponse, - RichTextElementResponse, isFileElementResponse, isRichTextElementResponse, + RichTextElementResponse, } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; import { CollaborativeTextEditorElementResponseMapper } from './collaborative-text-editor-element-response.mapper'; +import { DeletedElementResponseMapper } from './deleted-element-response.mapper'; import { DrawingElementResponseMapper } from './drawing-element-response.mapper'; import { ExternalToolElementResponseMapper } from './external-tool-element-response.mapper'; import { FileElementResponseMapper } from './file-element-response.mapper'; @@ -25,6 +26,7 @@ export class ContentElementResponseFactory { SubmissionContainerElementResponseMapper.getInstance(), ExternalToolElementResponseMapper.getInstance(), CollaborativeTextEditorElementResponseMapper.getInstance(), + DeletedElementResponseMapper.getInstance(), ]; static mapToResponse(element: AnyBoardNode): AnyContentElementResponse { diff --git a/apps/server/src/modules/board/controller/mapper/deleted-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/deleted-element-response.mapper.ts new file mode 100644 index 00000000000..26713849366 --- /dev/null +++ b/apps/server/src/modules/board/controller/mapper/deleted-element-response.mapper.ts @@ -0,0 +1,30 @@ +import { ContentElementType, DeletedElement } from '../../domain'; +import { DeletedElementContent, DeletedElementResponse, TimestampsResponse } from '../dto'; +import { BaseResponseMapper } from './base-mapper.interface'; + +export class DeletedElementResponseMapper implements BaseResponseMapper { + private static instance: DeletedElementResponseMapper; + + public static getInstance(): DeletedElementResponseMapper { + if (!DeletedElementResponseMapper.instance) { + DeletedElementResponseMapper.instance = new DeletedElementResponseMapper(); + } + + return DeletedElementResponseMapper.instance; + } + + mapToResponse(element: DeletedElement): DeletedElementResponse { + const result = new DeletedElementResponse({ + id: element.id, + timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), + type: ContentElementType.DELETED, + content: new DeletedElementContent({ title: element.title, deletedElementType: element.deletedElementType }), + }); + + return result; + } + + canMap(element: unknown): boolean { + return element instanceof DeletedElement; + } +} diff --git a/apps/server/src/modules/board/controller/mapper/index.ts b/apps/server/src/modules/board/controller/mapper/index.ts index 2d5e1b49937..980c5be1e45 100644 --- a/apps/server/src/modules/board/controller/mapper/index.ts +++ b/apps/server/src/modules/board/controller/mapper/index.ts @@ -10,3 +10,4 @@ export * from './link-element-response.mapper'; export * from './rich-text-element-response.mapper'; export * from './submission-container-element-response.mapper'; export * from './submission-item-response.mapper'; +export * from './deleted-element-response.mapper'; diff --git a/apps/server/src/modules/board/domain/board-node.factory.ts b/apps/server/src/modules/board/domain/board-node.factory.ts index 805bc20875f..ff59355567e 100644 --- a/apps/server/src/modules/board/domain/board-node.factory.ts +++ b/apps/server/src/modules/board/domain/board-node.factory.ts @@ -1,5 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { Injectable, NotImplementedException } from '@nestjs/common'; +import { Injectable, NotImplementedException, UnprocessableEntityException } from '@nestjs/common'; import { EntityId, InputFormat } from '@shared/domain/types'; import { Card } from './card.do'; import { CollaborativeTextEditorElement } from './collaborative-text-editor.do'; @@ -79,6 +79,8 @@ export class BoardNodeFactory { ...this.getBaseProps(), }); break; + case ContentElementType.DELETED: + throw new UnprocessableEntityException('Deleted elements cannot be created from the outside'); case ContentElementType.COLLABORATIVE_TEXT_EDITOR: element = new CollaborativeTextEditorElement({ ...this.getBaseProps(), diff --git a/apps/server/src/modules/board/domain/deleted-element.do.ts b/apps/server/src/modules/board/domain/deleted-element.do.ts new file mode 100644 index 00000000000..ce923872bc1 --- /dev/null +++ b/apps/server/src/modules/board/domain/deleted-element.do.ts @@ -0,0 +1,23 @@ +import { BoardNode } from './board-node.do'; +import type { ContentElementType, DeletedElementProps } from './types'; + +export class DeletedElement extends BoardNode { + get title(): string { + return this.props.title; + } + + set title(value: string) { + this.props.title = value; + } + + get deletedElementType(): ContentElementType { + return this.props.deletedElementType; + } + + canHaveChild(): boolean { + return false; + } +} + +export const isDeletedElement = (reference: unknown): reference is DeletedElement => + reference instanceof DeletedElement; diff --git a/apps/server/src/modules/board/domain/index.ts b/apps/server/src/modules/board/domain/index.ts index efc5293a454..a052556cf7f 100644 --- a/apps/server/src/modules/board/domain/index.ts +++ b/apps/server/src/modules/board/domain/index.ts @@ -16,3 +16,4 @@ export * from './submission-item.do'; export * from './path-utils'; export * from './types'; export * from './type-mapping'; +export * from './deleted-element.do'; diff --git a/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts b/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts index 7ea3d2f2c76..a6969807633 100644 --- a/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts +++ b/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts @@ -3,13 +3,3 @@ import type { MediaExternalToolElement } from '../media-external-tool-element.do import type { MediaLine } from '../media-line.do'; export type AnyMediaBoardNode = MediaBoard | MediaLine | MediaExternalToolElement; - -// TODO remove if not needed -// export type AnyMediaBoardNode = MediaExternalToolElement; -/* -export const isAnyMediaContentElement = (element: AnyMediaBoardNode): element is AnyMediaBoardNode => { - const result: boolean = element instanceof MediaExternalToolElement; - - return result; -}; -*/ diff --git a/apps/server/src/modules/board/domain/type-mapping.ts b/apps/server/src/modules/board/domain/type-mapping.ts index 983e9a2d332..a4a3b08ab8e 100644 --- a/apps/server/src/modules/board/domain/type-mapping.ts +++ b/apps/server/src/modules/board/domain/type-mapping.ts @@ -3,6 +3,7 @@ import { Card } from './card.do'; import { CollaborativeTextEditorElement } from './collaborative-text-editor.do'; import { ColumnBoard } from './colum-board.do'; import { Column } from './column.do'; +import { DeletedElement } from './deleted-element.do'; import { DrawingElement } from './drawing-element.do'; import { ExternalToolElement } from './external-tool-element.do'; import { FileElement } from './file-element.do'; @@ -30,6 +31,7 @@ const BoardNodeTypeToConstructor = { [BoardNodeType.RICH_TEXT_ELEMENT]: RichTextElement, [BoardNodeType.SUBMISSION_CONTAINER_ELEMENT]: SubmissionContainerElement, [BoardNodeType.SUBMISSION_ITEM]: SubmissionItem, + [BoardNodeType.DELETED_ELEMENT]: DeletedElement, } as const; export const getBoardNodeConstructor = (type: T): typeof BoardNodeTypeToConstructor[T] => diff --git a/apps/server/src/modules/board/domain/types/any-content-element.ts b/apps/server/src/modules/board/domain/types/any-content-element.ts index 1751287d35e..c8f6cae8bb3 100644 --- a/apps/server/src/modules/board/domain/types/any-content-element.ts +++ b/apps/server/src/modules/board/domain/types/any-content-element.ts @@ -1,4 +1,5 @@ import { type CollaborativeTextEditorElement, isCollaborativeTextEditorElement } from '../collaborative-text-editor.do'; +import { type DeletedElement, isDeletedElement } from '../deleted-element.do'; import { type DrawingElement, isDrawingElement } from '../drawing-element.do'; import { type ExternalToolElement, isExternalToolElement } from '../external-tool-element.do'; import { type FileElement, isFileElement } from '../file-element.do'; @@ -14,7 +15,8 @@ export type AnyContentElement = | FileElement | LinkElement | RichTextElement - | SubmissionContainerElement; + | SubmissionContainerElement + | DeletedElement; export const isContentElement = (boardNode: AnyBoardNode): boardNode is AnyContentElement => { const result = @@ -24,7 +26,8 @@ export const isContentElement = (boardNode: AnyBoardNode): boardNode is AnyConte isFileElement(boardNode) || isLinkElement(boardNode) || isRichTextElement(boardNode) || - isSubmissionContainerElement(boardNode); + isSubmissionContainerElement(boardNode) || + isDeletedElement(boardNode); return result; }; diff --git a/apps/server/src/modules/board/domain/types/board-node-props.ts b/apps/server/src/modules/board/domain/types/board-node-props.ts index a89dab6a802..f8307160192 100644 --- a/apps/server/src/modules/board/domain/types/board-node-props.ts +++ b/apps/server/src/modules/board/domain/types/board-node-props.ts @@ -1,8 +1,9 @@ import type { EntityId, InputFormat } from '@shared/domain/types'; +import type { MediaBoardColors } from '../media-board'; import type { AnyBoardNode } from './any-board-node'; import type { BoardExternalReference } from './board-external-reference'; import { BoardLayout } from './board-layout.enum'; -import type { MediaBoardColors } from '../media-board'; +import { ContentElementType } from './content-element-type.enum'; export interface BoardNodeProps { id: EntityId; @@ -65,6 +66,11 @@ export interface SubmissionItemProps extends BoardNodeProps { userId: EntityId; } +export interface DeletedElementProps extends BoardNodeProps { + title: string; + deletedElementType: ContentElementType; +} + export interface MediaBoardProps extends BoardNodeProps { context: BoardExternalReference; backgroundColor: MediaBoardColors; diff --git a/apps/server/src/modules/board/domain/types/board-node-type.enum.ts b/apps/server/src/modules/board/domain/types/board-node-type.enum.ts index 7fde3710af1..71523c41749 100644 --- a/apps/server/src/modules/board/domain/types/board-node-type.enum.ts +++ b/apps/server/src/modules/board/domain/types/board-node-type.enum.ts @@ -10,6 +10,7 @@ export enum BoardNodeType { SUBMISSION_ITEM = 'submission-item', EXTERNAL_TOOL = 'external-tool', COLLABORATIVE_TEXT_EDITOR = 'collaborative-text-editor', + DELETED_ELEMENT = 'deleted-element', MEDIA_BOARD = 'media-board', MEDIA_LINE = 'media-line', diff --git a/apps/server/src/modules/board/domain/types/content-element-type.enum.ts b/apps/server/src/modules/board/domain/types/content-element-type.enum.ts index 5127c9b4376..773c63fdbe9 100644 --- a/apps/server/src/modules/board/domain/types/content-element-type.enum.ts +++ b/apps/server/src/modules/board/domain/types/content-element-type.enum.ts @@ -6,4 +6,5 @@ export enum ContentElementType { SUBMISSION_CONTAINER = 'submissionContainer', EXTERNAL_TOOL = 'externalTool', COLLABORATIVE_TEXT_EDITOR = 'collaborativeTextEditor', + DELETED = 'deleted', } diff --git a/apps/server/src/modules/board/repo/entity/board-node.entity.ts b/apps/server/src/modules/board/repo/entity/board-node.entity.ts index a19ea9cea70..3385f29b8a8 100644 --- a/apps/server/src/modules/board/repo/entity/board-node.entity.ts +++ b/apps/server/src/modules/board/repo/entity/board-node.entity.ts @@ -2,8 +2,14 @@ import { Embedded, Entity, Enum, Index, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId, InputFormat } from '@shared/domain/types'; import { ObjectIdType } from '@shared/repo/types/object-id.type'; -import { AnyBoardNode, BoardLayout, BoardNodeType, ROOT_PATH } from '../../domain'; -import { MediaBoardColors } from '../../domain/media-board/types'; +import { + AnyBoardNode, + BoardLayout, + BoardNodeType, + ContentElementType, + MediaBoardColors, + ROOT_PATH, +} from '../../domain'; import type { BoardNodeEntityProps } from '../types'; import { Context } from './embeddables'; @@ -31,7 +37,7 @@ export class BoardNodeEntity extends BaseEntityWithTimestamps implements BoardNo @Property({ persist: false }) domainObject: AnyBoardNode | undefined; - // Card, Column, ColumnBoard, LinkElement, MedialLine + // Card, Column, ColumnBoard, LinkElement, MedialLine, DeletedElement // -------------------------------------------------------------------------- @Property({ nullable: true }) title: string | undefined; @@ -107,4 +113,9 @@ export class BoardNodeEntity extends BaseEntityWithTimestamps implements BoardNo @Property({ type: 'MediaBoardColors', nullable: true }) backgroundColor: MediaBoardColors | undefined; + + // DeletedElement + // -------------------------------------------------------------------------- + @Enum({ type: 'ContentElementType', nullable: true }) + deletedElementType: ContentElementType | undefined; } diff --git a/apps/server/src/modules/board/repo/types/board-node-entity-props.ts b/apps/server/src/modules/board/repo/types/board-node-entity-props.ts index 40c756264d0..29073bc3a2e 100644 --- a/apps/server/src/modules/board/repo/types/board-node-entity-props.ts +++ b/apps/server/src/modules/board/repo/types/board-node-entity-props.ts @@ -6,6 +6,7 @@ import type { CollaborativeTextEditorElementProps, ColumnBoardProps, ColumnProps, + DeletedElementProps, DrawingElementProps, ExternalToolElementProps, FileElementProps, @@ -55,4 +56,5 @@ export interface BoardNodeEntityProps ComponentProps, ComponentProps, ComponentProps, - ComponentProps {} + ComponentProps, + ComponentProps {} 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 0ac96b627b1..52c8279706c 100644 --- a/apps/server/src/modules/board/service/board-node.service.ts +++ b/apps/server/src/modules/board/service/board-node.service.ts @@ -52,6 +52,19 @@ export class BoardNodeService { await this.contentElementUpdateService.updateContent(element, content); } + async replace(oldNode: AnyBoardNode, newNode: AnyBoardNode): Promise { + const parent: AnyBoardNode | undefined = await this.findParent(oldNode); + + if (!parent) { + throw new NotFoundException(`Unable to find a parent node for ${oldNode.id}`); + } + + parent.addChild(newNode); + await this.boardNodeRepo.save(parent); + + await this.delete(oldNode); + } + async move(child: AnyBoardNode, targetParent: AnyBoardNode, targetPosition?: number): Promise { const saveList: AnyBoardNode[] = []; @@ -128,6 +141,12 @@ export class BoardNodeService { return rootNode; } + public async findElementsByContextExternalToolId(contextExternalToolId: EntityId): Promise { + const elements: AnyBoardNode[] = await this.boardNodeRepo.findByContextExternalToolIds([contextExternalToolId]); + + return elements; + } + async delete(boardNode: AnyBoardNode): Promise { const parent = await this.findParent(boardNode); if (parent) { diff --git a/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts b/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts new file mode 100644 index 00000000000..440a0d948b3 --- /dev/null +++ b/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts @@ -0,0 +1,35 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { ContextExternalToolDeletedEvent } from '@modules/tool/context-external-tool/domain'; +import { Injectable } from '@nestjs/common'; +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { Logger } from '@src/core/logger'; +import { AnyBoardNode, ContentElementType, DeletedElement, ROOT_PATH } from '../../domain'; +import { BoardNodeService } from '../board-node.service'; + +@Injectable() +@EventsHandler(ContextExternalToolDeletedEvent) +export class ContextExternalToolDeletedEventHandlerService implements IEventHandler { + constructor(private readonly boardNodeService: BoardNodeService, private readonly logger: Logger) { + this.logger.setContext(ContextExternalToolDeletedEventHandlerService.name); + } + + public async handle(event: ContextExternalToolDeletedEvent) { + const elements: AnyBoardNode[] = await this.boardNodeService.findElementsByContextExternalToolId(event.id); + + elements.map(async (element: AnyBoardNode): Promise => { + const placeholder: DeletedElement = new DeletedElement({ + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedElementType: ContentElementType.EXTERNAL_TOOL, + title: event.title, + }); + + await this.boardNodeService.replace(element, placeholder); + }); + } +} 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 8d786a7293e..3870411bac9 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 @@ -14,6 +14,7 @@ import { CollaborativeTextEditorElement, Column, ColumnBoard, + DeletedElement, DrawingElement, ExternalToolElement, FileElement, @@ -79,6 +80,9 @@ export class BoardNodeCopyService { case BoardNodeType.COLLABORATIVE_TEXT_EDITOR: result = await this.copyCollaborativeTextEditorElement(boardNode as CollaborativeTextEditorElement, context); break; + case BoardNodeType.DELETED_ELEMENT: + result = await this.copyDeletedElement(boardNode as DeletedElement, context); + break; case BoardNodeType.MEDIA_BOARD: result = await this.copyMediaBoard(boardNode as MediaBoard, context); break; @@ -365,6 +369,22 @@ export class BoardNodeCopyService { return Promise.resolve(result); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copyDeletedElement(original: DeletedElement, context: CopyContext): Promise { + const copy = new DeletedElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.DELETED_ELEMENT, + status: CopyStatusEnum.SUCCESS, + }; + + return Promise.resolve(result); + } + // ---- private async copyChildrenOf(boardNode: AnyBoardNode, context: CopyContext): Promise { diff --git a/apps/server/src/modules/board/testing/deleted-element.factory.ts b/apps/server/src/modules/board/testing/deleted-element.factory.ts new file mode 100644 index 00000000000..f71b1263585 --- /dev/null +++ b/apps/server/src/modules/board/testing/deleted-element.factory.ts @@ -0,0 +1,20 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { ContentElementType, DeletedElement, DeletedElementProps, ROOT_PATH } from '../domain'; + +export const deletedElementFactory = BaseFactory.define( + DeletedElement, + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + title: `Title #${sequence}`, + deletedElementType: ContentElementType.EXTERNAL_TOOL, + }; + } +); diff --git a/apps/server/src/modules/board/testing/index.ts b/apps/server/src/modules/board/testing/index.ts index 7e6e20039e8..898a9b9f965 100644 --- a/apps/server/src/modules/board/testing/index.ts +++ b/apps/server/src/modules/board/testing/index.ts @@ -16,3 +16,4 @@ export * from './media-line.factory'; export * from './rich-text-element.factory'; export * from './submission-container-element.factory'; export * from './submission-item.factory'; +export * from './deleted-element.factory'; diff --git a/apps/server/src/modules/copy-helper/types/copy.types.ts b/apps/server/src/modules/copy-helper/types/copy.types.ts index 27416d40303..703ab28aa56 100644 --- a/apps/server/src/modules/copy-helper/types/copy.types.ts +++ b/apps/server/src/modules/copy-helper/types/copy.types.ts @@ -20,6 +20,7 @@ export enum CopyElementType { CONTENT = 'CONTENT', COURSE = 'COURSE', COURSEGROUP_GROUP = 'COURSEGROUP_GROUP', + DELETED_ELEMENT = 'DELETED_ELEMENT', EXTERNAL_TOOL = 'EXTERNAL_TOOL', EXTERNAL_TOOL_ELEMENT = 'EXTERNAL_TOOL_ELEMENT', FILE = 'FILE', diff --git a/apps/server/src/modules/tool/common/common-tool.module.ts b/apps/server/src/modules/tool/common/common-tool.module.ts index 535e97f448e..a48b814900f 100644 --- a/apps/server/src/modules/tool/common/common-tool.module.ts +++ b/apps/server/src/modules/tool/common/common-tool.module.ts @@ -1,27 +1,32 @@ import { BoardModule } from '@modules/board'; import { forwardRef, Module } from '@nestjs/common'; -import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { CqrsModule } from '@nestjs/cqrs'; +import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { SchoolModule } from '@src/modules/school'; -import { CommonToolService, CommonToolValidationService } from './service'; +import { CommonToolDeleteService, CommonToolService, CommonToolValidationService } from './service'; import { CommonToolMetadataService } from './service/common-tool-metadata.service'; @Module({ - imports: [LoggerModule, SchoolModule, forwardRef(() => BoardModule)], + imports: [LoggerModule, SchoolModule, forwardRef(() => BoardModule), CqrsModule], // TODO: make deletion of entities cascading, adjust ExternalToolService.deleteExternalTool and remove the repos from here providers: [ CommonToolService, CommonToolValidationService, + ExternalToolRepo, SchoolExternalToolRepo, ContextExternalToolRepo, CommonToolMetadataService, + CommonToolDeleteService, ], exports: [ CommonToolService, CommonToolValidationService, + ExternalToolRepo, SchoolExternalToolRepo, ContextExternalToolRepo, CommonToolMetadataService, + CommonToolDeleteService, ], }) export class CommonToolModule {} diff --git a/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts b/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts new file mode 100644 index 00000000000..b3ebf7326fc --- /dev/null +++ b/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { EventBus } from '@nestjs/cqrs'; +import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { ContextExternalTool, ContextExternalToolDeletedEvent } from '../../context-external-tool/domain'; +import type { ExternalTool } from '../../external-tool/domain'; +import type { SchoolExternalTool } from '../../school-external-tool/domain'; + +@Injectable() +export class CommonToolDeleteService { + constructor( + private readonly externalToolRepo: ExternalToolRepo, + private readonly schoolExternalToolRepo: SchoolExternalToolRepo, + private readonly contextExternalToolRepo: ContextExternalToolRepo, + private readonly eventBus: EventBus + ) {} + + public async deleteExternalTool(externalTool: ExternalTool): Promise { + await this.externalToolRepo.deleteById(externalTool.id); + + const schoolExternalTools: SchoolExternalTool[] = await this.schoolExternalToolRepo.findByExternalToolId( + externalTool.id + ); + + const promises: Promise[] = schoolExternalTools.map(async (schoolExternalTool) => { + await this.deleteSchoolExternalToolInternal(externalTool, schoolExternalTool); + }); + + await Promise.all(promises); + } + + public async deleteSchoolExternalTool(schoolExternalTool: SchoolExternalTool): Promise { + const externalTool: ExternalTool = await this.externalToolRepo.findById(schoolExternalTool.toolId); + + await this.deleteSchoolExternalToolInternal(externalTool, schoolExternalTool); + } + + public async deleteContextExternalTool(contextExternalTool: ContextExternalTool): Promise { + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolRepo.findById( + contextExternalTool.schoolToolRef.schoolToolId + ); + + const externalTool: ExternalTool = await this.externalToolRepo.findById(schoolExternalTool.toolId); + + await this.deleteContextExternalToolInternal(externalTool, contextExternalTool); + } + + private async deleteSchoolExternalToolInternal( + externalTool: ExternalTool, + schoolExternalTool: SchoolExternalTool + ): Promise { + await this.schoolExternalToolRepo.deleteById(schoolExternalTool.id); + + const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolRepo.find({ + schoolToolRef: { schoolToolId: schoolExternalTool.id }, + }); + + const promises: Promise[] = contextExternalTools.map(async (contextExternalTool) => { + await this.deleteContextExternalToolInternal(externalTool, contextExternalTool); + }); + + await Promise.all(promises); + } + + private async deleteContextExternalToolInternal( + externalTool: ExternalTool, + contextExternalTool: ContextExternalTool + ): Promise { + await this.contextExternalToolRepo.delete(contextExternalTool); + + this.eventBus.publish( + new ContextExternalToolDeletedEvent({ + id: contextExternalTool.id, + title: contextExternalTool.displayName ?? externalTool.name, + }) + ); + } +} diff --git a/apps/server/src/modules/tool/common/service/index.ts b/apps/server/src/modules/tool/common/service/index.ts index b7f626f1e42..9a6567dbbcf 100644 --- a/apps/server/src/modules/tool/common/service/index.ts +++ b/apps/server/src/modules/tool/common/service/index.ts @@ -1,2 +1,3 @@ export * from './common-tool.service'; export { CommonToolValidationService, ToolParameterTypeValidationUtil } from './validation'; +export { CommonToolDeleteService } from './common-tool-delete.service'; diff --git a/apps/server/src/modules/tool/context-external-tool/domain/event/context-external-tool-deleted.event.ts b/apps/server/src/modules/tool/context-external-tool/domain/event/context-external-tool-deleted.event.ts new file mode 100644 index 00000000000..a449fc3f54f --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/event/context-external-tool-deleted.event.ts @@ -0,0 +1,12 @@ +import { EntityId } from '@shared/domain/types'; + +export class ContextExternalToolDeletedEvent { + id: EntityId; + + title: string; + + constructor(props: ContextExternalToolDeletedEvent) { + this.id = props.id; + this.title = props.title; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/event/index.ts b/apps/server/src/modules/tool/context-external-tool/domain/event/index.ts new file mode 100644 index 00000000000..825ef2f3479 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/event/index.ts @@ -0,0 +1 @@ +export * from './context-external-tool-deleted.event'; diff --git a/apps/server/src/modules/tool/context-external-tool/domain/index.ts b/apps/server/src/modules/tool/context-external-tool/domain/index.ts index 17704725505..bb51be61682 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/index.ts @@ -1,4 +1,5 @@ export * from './context-external-tool.do'; export * from './context-ref'; export * from './tool-reference'; +export * from './event'; export { RestrictedContextMismatchLoggableException } from './error'; diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts index 7ba0878bfd3..43f7d5f82ae 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { ContextExternalToolRepo } from '@shared/repo'; import { CustomParameter, CustomParameterEntry } from '../../common/domain'; -import { CommonToolService } from '../../common/service'; +import { CommonToolDeleteService, CommonToolService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; @@ -22,7 +22,8 @@ export class ContextExternalToolService { private readonly contextExternalToolRepo: ContextExternalToolRepo, private readonly externalToolService: ExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, - private readonly commonToolService: CommonToolService + private readonly commonToolService: CommonToolService, + private readonly commonToolDeleteService: CommonToolDeleteService ) {} public async findContextExternalTools(query: ContextExternalToolQuery): Promise { @@ -49,18 +50,8 @@ export class ContextExternalToolService { return savedContextExternalTool; } - public async deleteBySchoolExternalToolId(schoolExternalToolId: EntityId) { - const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolRepo.find({ - schoolToolRef: { - schoolToolId: schoolExternalToolId, - }, - }); - - await this.contextExternalToolRepo.delete(contextExternalTools); - } - public async deleteContextExternalTool(contextExternalTool: ContextExternalTool): Promise { - await this.contextExternalToolRepo.delete(contextExternalTool); + await this.commonToolDeleteService.deleteContextExternalTool(contextExternalTool); } public async findAllByContext(contextRef: ContextRef): Promise { 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 695e4cad02a..cdb07b5f74c 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 @@ -2,7 +2,6 @@ import { EncryptionModule } from '@infra/encryption'; import { OauthProviderServiceModule } from '@modules/oauth-provider'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { ExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { InstanceModule } from '../../instance'; import { CommonToolModule } from '../common'; @@ -29,7 +28,6 @@ import { ExternalToolValidationService, ExternalToolConfigurationService, ExternalToolLogoService, - ExternalToolRepo, ExternalToolMetadataMapper, ToolContextMapper, DatasheetPdfService, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index ae477c00692..5d8a9e2a502 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -9,7 +9,7 @@ import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } fro import { LegacyLogger } from '@src/core/logger'; import { TokenEndpointAuthMethod } from '../../common/enum'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { CommonToolDeleteService } from '../../common/service'; import { ExternalTool, Oauth2ToolConfig } from '../domain'; import { ExternalToolServiceMapper } from './external-tool-service.mapper'; @@ -22,7 +22,8 @@ export class ExternalToolService { private readonly schoolExternalToolRepo: SchoolExternalToolRepo, private readonly contextExternalToolRepo: ContextExternalToolRepo, @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, - private readonly legacyLogger: LegacyLogger + private readonly legacyLogger: LegacyLogger, + private readonly commonToolDeleteService: CommonToolDeleteService ) {} public async createExternalTool(externalTool: ExternalTool): Promise { @@ -108,17 +109,8 @@ export class ExternalToolService { return externalTool; } - public async deleteExternalTool(toolId: EntityId): Promise { - const schoolExternalTools: SchoolExternalTool[] = await this.schoolExternalToolRepo.findByExternalToolId(toolId); - const schoolExternalToolIds: string[] = schoolExternalTools.map( - (schoolExternalTool: SchoolExternalTool): string => schoolExternalTool.id - ); - - await Promise.all([ - this.contextExternalToolRepo.deleteBySchoolExternalToolIds(schoolExternalToolIds), - this.schoolExternalToolRepo.deleteByExternalToolId(toolId), - this.externalToolRepo.deleteById(toolId), - ]); + public async deleteExternalTool(externalTool: ExternalTool): Promise { + await this.commonToolDeleteService.deleteExternalTool(externalTool); } private async updateOauth2ToolConfig(toUpdate: ExternalTool) { diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts index dbdc435035d..bc79c03f090 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts @@ -177,11 +177,13 @@ export class ExternalToolUc { } public async deleteExternalTool(userId: EntityId, externalToolId: EntityId, jwt: string): Promise { + const externalTool: ExternalTool = await this.externalToolService.findById(externalToolId); + await this.ensurePermission(userId, Permission.TOOL_ADMIN); await this.externalToolImageService.deleteAllFiles(externalToolId, jwt); - await this.externalToolService.deleteExternalTool(externalToolId); + await this.externalToolService.deleteExternalTool(externalTool); } public async getMetadataForExternalTool(userId: EntityId, toolId: EntityId): Promise { 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 978f0aa4ee5..809cbd94f3c 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 @@ -2,7 +2,7 @@ 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 { CommonToolDeleteService, CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool, SchoolExternalToolConfigurationStatus } from '../domain'; @@ -13,7 +13,8 @@ export class SchoolExternalToolService { constructor( private readonly schoolExternalToolRepo: SchoolExternalToolRepo, private readonly externalToolService: ExternalToolService, - private readonly commonToolValidationService: CommonToolValidationService + private readonly commonToolValidationService: CommonToolValidationService, + private readonly commonToolDeleteService: CommonToolDeleteService ) {} public async findById(schoolExternalToolId: EntityId): Promise { @@ -79,8 +80,8 @@ export class SchoolExternalToolService { return status; } - public deleteSchoolExternalToolById(schoolExternalToolId: EntityId): void { - this.schoolExternalToolRepo.deleteById(schoolExternalToolId); + public async deleteSchoolExternalTool(schoolExternalTool: SchoolExternalTool): Promise { + await this.commonToolDeleteService.deleteSchoolExternalTool(schoolExternalTool); } public async saveSchoolExternalTool(schoolExternalTool: SchoolExternalTool): Promise { diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts index 30919794391..1648171c5ac 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts @@ -211,24 +211,16 @@ describe('SchoolExternalToolUc', () => { return { userId: user.id, - schoolExternalToolId: tool.id, + tool, }; }; - it('should call the courseExternalToolService', async () => { - const { userId, schoolExternalToolId } = setup(); - - await uc.deleteSchoolExternalTool(userId, schoolExternalToolId); - - expect(contextExternalToolService.deleteBySchoolExternalToolId).toHaveBeenCalledWith(schoolExternalToolId); - }); - - it('should call the schoolExternalToolService', async () => { - const { userId, schoolExternalToolId } = setup(); + it('should delete the tool', async () => { + const { userId, tool } = setup(); - await uc.deleteSchoolExternalTool(userId, schoolExternalToolId); + await uc.deleteSchoolExternalTool(userId, tool.id); - expect(schoolExternalToolService.deleteSchoolExternalToolById).toHaveBeenCalledWith(schoolExternalToolId); + expect(schoolExternalToolService.deleteSchoolExternalTool).toHaveBeenCalledWith(tool); }); }); }); 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 c7861f82823..d57bfafa5ea 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 @@ -5,7 +5,6 @@ import { Permission } from '@shared/domain/interface'; 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, SchoolExternalToolProps } from '../domain'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; @@ -14,7 +13,6 @@ import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; export class SchoolExternalToolUc { constructor( private readonly schoolExternalToolService: SchoolExternalToolService, - private readonly contextExternalToolService: ContextExternalToolService, private readonly schoolExternalToolValidationService: SchoolExternalToolValidationService, private readonly commonToolMetadataService: CommonToolMetadataService, @Inject(forwardRef(() => AuthorizationService)) private readonly authorizationService: AuthorizationService, @@ -33,6 +31,7 @@ export class SchoolExternalToolUc { await this.ensureSchoolPermissions(user, tools, school, context); } + return tools; } @@ -75,10 +74,7 @@ export class SchoolExternalToolUc { const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); this.authorizationService.checkPermission(user, school, context); - await Promise.all([ - this.contextExternalToolService.deleteBySchoolExternalToolId(schoolExternalToolId), - this.schoolExternalToolService.deleteSchoolExternalToolById(schoolExternalToolId), - ]); + await this.schoolExternalToolService.deleteSchoolExternalTool(schoolExternalTool); } async getSchoolExternalTool(userId: EntityId, schoolExternalToolId: EntityId): Promise { @@ -114,6 +110,7 @@ export class SchoolExternalToolUc { }); const saved: SchoolExternalTool = await this.schoolExternalToolService.saveSchoolExternalTool(updated); + return saved; } 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 7502ecfdce5..7cc8596c6e5 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 @@ -299,7 +299,7 @@ describe(SchoolExternalToolRepo.name, () => { it('should delete a SchoolExternalTool', async () => { const { schoolExternalTool1 } = await setup(); - repo.deleteById(schoolExternalTool1.id); + await repo.deleteById(schoolExternalTool1.id); const result: SchoolExternalTool[] = await repo.find({ schoolId: schoolExternalTool1.school.id }); 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 42aa69db1ca..f88dfd3ec1e 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 @@ -48,8 +48,8 @@ export class SchoolExternalToolRepo { return domainObject; } - public deleteById(id: EntityId): void { - this.em.remove(this.em.getReference(this.entityName, id)); + public async deleteById(id: EntityId): Promise { + return this.em.removeAndFlush(this.em.getReference(this.entityName, id)); } async findByExternalToolId(toolId: string): Promise { From 2d9d0f801182297b0bca88608689404285dede5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 16 Aug 2024 10:13:04 +0200 Subject: [PATCH 2/8] add event handler to module --- apps/server/src/modules/board/board.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 884db4ee05a..bbb13662ec9 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -19,6 +19,7 @@ import { MediaBoardService, UserDeletedEventHandlerService, } from './service'; +import { ContextExternalToolDeletedEventHandlerService } from './service/event/context-external-tool-deleted-event-handler.service'; import { BoardContextService, BoardNodeCopyService, @@ -60,6 +61,7 @@ import { ColumnBoardReferenceService, ColumnBoardTitleService, UserDeletedEventHandlerService, + ContextExternalToolDeletedEventHandlerService, // TODO replace by import of MediaBoardModule (fix dependency cycle) MediaBoardService, ], From ff13b655205784c85f4ce7902f706a2aa27f8716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 16 Aug 2024 15:54:23 +0200 Subject: [PATCH 3/8] tests --- apps/server/src/modules/board/board.module.ts | 2 +- .../src/modules/board/domain/board-node.do.ts | 4 + .../board/service/board-node.service.spec.ts | 99 ++++++- ...tool-deleted-event-handler.service.spec.ts | 78 ++++++ ...rnal-tool-deleted-event-handler.service.ts | 5 +- .../src/modules/board/service/event/index.ts | 1 + .../board-node-copy-general.service.spec.ts | 14 + .../common-tool-delete.service.spec.ts | 262 ++++++++++++++++++ .../context-external-tool.service.spec.ts | 70 ++--- .../testing/context-external-tool.factory.ts | 7 +- .../service/external-tool.service.spec.ts | 43 +-- .../external-tool/uc/external-tool.uc.spec.ts | 7 +- .../school-external-tool.service.spec.ts | 26 +- .../uc/school-external-tool.uc.spec.ts | 7 - 14 files changed, 513 insertions(+), 112 deletions(-) create mode 100644 apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.spec.ts create mode 100644 apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index bbb13662ec9..798366fb0b7 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -16,10 +16,10 @@ import { BoardNodeAuthorizableService, BoardNodeService, ColumnBoardService, + ContextExternalToolDeletedEventHandlerService, MediaBoardService, UserDeletedEventHandlerService, } from './service'; -import { ContextExternalToolDeletedEventHandlerService } from './service/event/context-external-tool-deleted-event-handler.service'; import { BoardContextService, BoardNodeCopyService, diff --git a/apps/server/src/modules/board/domain/board-node.do.ts b/apps/server/src/modules/board/domain/board-node.do.ts index 8982b67c71d..1a7a74bbff0 100644 --- a/apps/server/src/modules/board/domain/board-node.do.ts +++ b/apps/server/src/modules/board/domain/board-node.do.ts @@ -28,6 +28,10 @@ export abstract class BoardNode extends DomainObject { }); }); }); + + describe('replace', () => { + describe('when replacing a node', () => { + const setup = () => { + const oldNode = externalToolElementFactory.build(); + const newNode = deletedElementFactory.build(); + const parentNode = cardFactory.build(); + parentNode.addChild(oldNode); + + boardNodeRepo.findById.mockResolvedValueOnce(new Card({ ...parentNode.getTrueProps() })); + + return { + parentNode, + oldNode, + newNode, + }; + }; + + it('should add the new node', async () => { + const { parentNode, oldNode, newNode } = setup(); + + await service.replace(oldNode, newNode); + + expect(boardNodeRepo.save).toHaveBeenCalledWith( + new Card({ ...parentNode.getTrueProps(), children: [oldNode, newNode] }) + ); + }); + + it('should delete the old node', async () => { + const { oldNode, newNode } = setup(); + + await service.replace(oldNode, newNode); + + expect(boardNodeRepo.delete).toHaveBeenCalledWith(oldNode); + }); + }); + + describe('when the node has no parent', () => { + const setup = () => { + const oldNode = externalToolElementFactory.build(); + const newNode = deletedElementFactory.build(); + + return { + oldNode, + newNode, + }; + }; + + it('should throw an error', async () => { + const { oldNode, newNode } = setup(); + + await expect(service.replace(oldNode, newNode)).rejects.toThrow(NotFoundException); + }); + }); + }); + + describe('findElementsByContextExternalToolId', () => { + describe('when finding a node by its ', () => { + const setup = () => { + const contextExternalToolId = new ObjectId().toHexString(); + const node = externalToolElementFactory.build({ + contextExternalToolId, + }); + + boardNodeRepo.findByContextExternalToolIds.mockResolvedValueOnce([node]); + + return { + node, + contextExternalToolId, + }; + }; + + it('should search by the context external tool id', async () => { + const { contextExternalToolId } = setup(); + + await service.findElementsByContextExternalToolId(contextExternalToolId); + + expect(boardNodeRepo.findByContextExternalToolIds).toHaveBeenCalledWith([contextExternalToolId]); + }); + + it('should return the node', async () => { + const { node, contextExternalToolId } = setup(); + + const result = await service.findElementsByContextExternalToolId(contextExternalToolId); + + expect(result).toEqual([node]); + }); + }); + }); }); diff --git a/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.spec.ts b/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.spec.ts new file mode 100644 index 00000000000..696648578bf --- /dev/null +++ b/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.spec.ts @@ -0,0 +1,78 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ContextExternalToolDeletedEvent } from '../../../tool/context-external-tool/domain'; +import { ContentElementType, DeletedElement, ROOT_PATH } from '../../domain'; +import { externalToolElementFactory } from '../../testing'; +import { BoardNodeService } from '../board-node.service'; +import { ContextExternalToolDeletedEventHandlerService } from './context-external-tool-deleted-event-handler.service'; + +describe(ContextExternalToolDeletedEventHandlerService.name, () => { + let module: TestingModule; + let service: ContextExternalToolDeletedEventHandlerService; + + let boardNodeService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ContextExternalToolDeletedEventHandlerService, + { + provide: BoardNodeService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ContextExternalToolDeletedEventHandlerService); + boardNodeService = module.get(BoardNodeService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('handle', () => { + describe('when a context external tool gets deleted', () => { + const setup = () => { + const contextExternalToolId = new ObjectId().toHexString(); + const event = new ContextExternalToolDeletedEvent({ id: contextExternalToolId, title: 'Delete me' }); + const externalToolElement = externalToolElementFactory.build({ + contextExternalToolId, + }); + + boardNodeService.findElementsByContextExternalToolId.mockResolvedValueOnce([externalToolElement]); + + return { + event, + externalToolElement, + }; + }; + + it('should replace the context external tool element with a deleted element', async () => { + const { event, externalToolElement } = setup(); + + await service.handle(event); + + expect(boardNodeService.replace).toHaveBeenCalledWith( + externalToolElement, + new DeletedElement({ + id: expect.any(String), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: expect.any(Date) as unknown as Date, + updatedAt: expect.any(Date) as unknown as Date, + deletedElementType: ContentElementType.EXTERNAL_TOOL, + title: event.title, + }) + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts b/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts index 440a0d948b3..21a979dbc91 100644 --- a/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts +++ b/apps/server/src/modules/board/service/event/context-external-tool-deleted-event-handler.service.ts @@ -2,16 +2,13 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ContextExternalToolDeletedEvent } from '@modules/tool/context-external-tool/domain'; import { Injectable } from '@nestjs/common'; import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { Logger } from '@src/core/logger'; import { AnyBoardNode, ContentElementType, DeletedElement, ROOT_PATH } from '../../domain'; import { BoardNodeService } from '../board-node.service'; @Injectable() @EventsHandler(ContextExternalToolDeletedEvent) export class ContextExternalToolDeletedEventHandlerService implements IEventHandler { - constructor(private readonly boardNodeService: BoardNodeService, private readonly logger: Logger) { - this.logger.setContext(ContextExternalToolDeletedEventHandlerService.name); - } + constructor(private readonly boardNodeService: BoardNodeService) {} public async handle(event: ContextExternalToolDeletedEvent) { const elements: AnyBoardNode[] = await this.boardNodeService.findElementsByContextExternalToolId(event.id); diff --git a/apps/server/src/modules/board/service/event/index.ts b/apps/server/src/modules/board/service/event/index.ts index fb9b581be6a..3be8dbc381d 100644 --- a/apps/server/src/modules/board/service/event/index.ts +++ b/apps/server/src/modules/board/service/event/index.ts @@ -1 +1,2 @@ export { UserDeletedEventHandlerService } from './user-deleted-event-handler.service'; +export { ContextExternalToolDeletedEventHandlerService } from './context-external-tool-deleted-event-handler.service'; 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 a660a008ec9..1c6c4bfafe9 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 @@ -13,6 +13,7 @@ import { collaborativeTextEditorFactory, columnBoardFactory, columnFactory, + deletedElementFactory, drawingElementFactory, externalToolElementFactory, fileElementFactory, @@ -94,6 +95,7 @@ describe(BoardNodeCopyService.name, () => { jest.spyOn(service, 'copyMediaBoard').mockResolvedValue(mockStatus); jest.spyOn(service, 'copyMediaLine').mockResolvedValue(mockStatus); jest.spyOn(service, 'copyMediaExternalToolElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyDeletedElement').mockResolvedValue(mockStatus); return { copyContext, mockStatus }; }; @@ -265,6 +267,18 @@ describe(BoardNodeCopyService.name, () => { expect(result).toEqual(mockStatus); }); }); + + describe('when called with deleted element', () => { + it('should copy deleted element', async () => { + const { copyContext, mockStatus } = setup(); + const node = deletedElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyDeletedElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); }); }); }); diff --git a/apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts b/apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts new file mode 100644 index 00000000000..6e6023dbcd5 --- /dev/null +++ b/apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts @@ -0,0 +1,262 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { EventBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { ContextExternalToolDeletedEvent } from '../../context-external-tool/domain'; +import { contextExternalToolFactory } from '../../context-external-tool/testing'; +import { externalToolFactory } from '../../external-tool/testing'; +import { SchoolExternalToolRef } from '../../school-external-tool/domain'; +import { schoolExternalToolFactory } from '../../school-external-tool/testing'; +import { CommonToolDeleteService } from './common-tool-delete.service'; + +describe(CommonToolDeleteService.name, () => { + let module: TestingModule; + let service: CommonToolDeleteService; + + let externalToolRepo: DeepMocked; + let schoolExternalToolRepo: DeepMocked; + let contextExternalToolRepo: DeepMocked; + let eventBus: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + CommonToolDeleteService, + { + provide: ExternalToolRepo, + useValue: createMock(), + }, + { + provide: SchoolExternalToolRepo, + useValue: createMock(), + }, + { + provide: ContextExternalToolRepo, + useValue: createMock(), + }, + { + provide: EventBus, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(CommonToolDeleteService); + externalToolRepo = module.get(ExternalToolRepo); + schoolExternalToolRepo = module.get(SchoolExternalToolRepo); + contextExternalToolRepo = module.get(ContextExternalToolRepo); + eventBus = module.get(EventBus); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('deleteExternalTool', () => { + describe('when deleting an external tool', () => { + const setup = () => { + const externalTool = externalToolFactory.build(); + const schoolExternalTool = schoolExternalToolFactory.build({ + toolId: externalTool.id, + }); + const displayName = 'test'; + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: new SchoolExternalToolRef({ + schoolToolId: schoolExternalTool.id, + }), + displayName, + }); + + externalToolRepo.findById.mockResolvedValueOnce(externalTool); + schoolExternalToolRepo.findByExternalToolId.mockResolvedValueOnce([schoolExternalTool]); + contextExternalToolRepo.find.mockResolvedValueOnce([contextExternalTool]); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + displayName, + }; + }; + + it('should delete the external tool', async () => { + const { externalTool } = setup(); + + await service.deleteExternalTool(externalTool); + + expect(externalToolRepo.deleteById).toHaveBeenCalledWith(externalTool.id); + }); + + it('should delete the school external tools', async () => { + const { externalTool, schoolExternalTool } = setup(); + + await service.deleteExternalTool(externalTool); + + expect(schoolExternalToolRepo.deleteById).toHaveBeenCalledWith(schoolExternalTool.id); + }); + + it('should delete the context external tools', async () => { + const { externalTool, contextExternalTool } = setup(); + + await service.deleteExternalTool(externalTool); + + expect(contextExternalToolRepo.delete).toHaveBeenCalledWith(contextExternalTool); + }); + + it('should use the correct school external tools', async () => { + const { externalTool } = setup(); + + await service.deleteExternalTool(externalTool); + + expect(schoolExternalToolRepo.findByExternalToolId).toHaveBeenCalledWith(externalTool.id); + }); + + it('should use the correct context external tools', async () => { + const { externalTool, schoolExternalTool } = setup(); + + await service.deleteExternalTool(externalTool); + + expect(contextExternalToolRepo.find).toHaveBeenCalledWith({ + schoolToolRef: { schoolToolId: schoolExternalTool.id }, + }); + }); + + it('should publish a delete event for the context external tools', async () => { + const { externalTool, contextExternalTool, displayName } = setup(); + + await service.deleteExternalTool(externalTool); + + expect(eventBus.publish).toHaveBeenCalledWith( + new ContextExternalToolDeletedEvent({ + id: contextExternalTool.id, + title: displayName, + }) + ); + }); + }); + }); + + describe('deleteSchoolExternalTool', () => { + describe('when deleting a school external tool', () => { + const setup = () => { + const externalTool = externalToolFactory.build(); + const schoolExternalTool = schoolExternalToolFactory.build({ + toolId: externalTool.id, + }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: new SchoolExternalToolRef({ + schoolToolId: schoolExternalTool.id, + }), + displayName: undefined, + }); + + externalToolRepo.findById.mockResolvedValueOnce(externalTool); + contextExternalToolRepo.find.mockResolvedValueOnce([contextExternalTool]); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should delete the school external tool', async () => { + const { schoolExternalTool } = setup(); + + await service.deleteSchoolExternalTool(schoolExternalTool); + + expect(schoolExternalToolRepo.deleteById).toHaveBeenCalledWith(schoolExternalTool.id); + }); + + it('should delete the context external tools', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + await service.deleteSchoolExternalTool(schoolExternalTool); + + expect(contextExternalToolRepo.delete).toHaveBeenCalledWith(contextExternalTool); + }); + + it('should use the correct context external tools', async () => { + const { schoolExternalTool } = setup(); + + await service.deleteSchoolExternalTool(schoolExternalTool); + + expect(contextExternalToolRepo.find).toHaveBeenCalledWith({ + schoolToolRef: { schoolToolId: schoolExternalTool.id }, + }); + }); + + it('should publish a delete event for the context external tools', async () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.deleteSchoolExternalTool(schoolExternalTool); + + expect(eventBus.publish).toHaveBeenCalledWith( + new ContextExternalToolDeletedEvent({ + id: contextExternalTool.id, + title: externalTool.name, + }) + ); + }); + }); + }); + + describe('deleteContextExternalTool', () => { + describe('when deleting a context external tool', () => { + const setup = () => { + const externalTool = externalToolFactory.build(); + const schoolExternalTool = schoolExternalToolFactory.build({ + toolId: externalTool.id, + }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: new SchoolExternalToolRef({ + schoolToolId: schoolExternalTool.id, + }), + displayName: undefined, + }); + + schoolExternalToolRepo.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolRepo.findById.mockResolvedValueOnce(externalTool); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should delete the context external tool', async () => { + const { contextExternalTool } = setup(); + + await service.deleteContextExternalTool(contextExternalTool); + + expect(contextExternalToolRepo.delete).toHaveBeenCalledWith(contextExternalTool); + }); + + it('should use the correct school external tools', async () => { + const { contextExternalTool } = setup(); + + await service.deleteContextExternalTool(contextExternalTool); + + expect(schoolExternalToolRepo.findById).toHaveBeenCalledWith(contextExternalTool.schoolToolRef.schoolToolId); + }); + + it('should publish a delete event for the context external tools', async () => { + const { externalTool, contextExternalTool } = setup(); + + await service.deleteContextExternalTool(contextExternalTool); + + expect(eventBus.publish).toHaveBeenCalledWith( + new ContextExternalToolDeletedEvent({ + id: contextExternalTool.id, + title: externalTool.name, + }) + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts index 1aeac5675ff..deaf6330fd5 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts @@ -8,11 +8,10 @@ import { ContextExternalToolRepo } from '@shared/repo'; import { legacySchoolDoFactory } from '@shared/testing'; import { CustomParameter } from '../../common/domain'; import { ToolContextType } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; +import { CommonToolDeleteService, CommonToolService } from '../../common/service'; import { ExternalToolService } from '../../external-tool'; import { ExternalTool } from '../../external-tool/domain'; -import { externalToolFactory } from '../../external-tool/testing'; -import { customParameterFactory } from '../../external-tool/testing/external-tool.factory'; +import { customParameterFactory, externalToolFactory } from '../../external-tool/testing'; import { SchoolExternalToolService } from '../../school-external-tool'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; @@ -28,10 +27,11 @@ import { ContextExternalToolService } from './context-external-tool.service'; describe(ContextExternalToolService.name, () => { let module: TestingModule; let service: ContextExternalToolService; + let externalToolService: DeepMocked; let schoolExternalToolService: DeepMocked; let commonToolService: DeepMocked; - + let commonToolDeleteService: DeepMocked; let contextExternalToolRepo: DeepMocked; beforeAll(async () => { @@ -58,6 +58,10 @@ describe(ContextExternalToolService.name, () => { provide: CommonToolService, useValue: createMock(), }, + { + provide: CommonToolDeleteService, + useValue: createMock(), + }, ], }).compile(); @@ -66,6 +70,7 @@ describe(ContextExternalToolService.name, () => { externalToolService = module.get(ExternalToolService); schoolExternalToolService = module.get(SchoolExternalToolService); commonToolService = module.get(CommonToolService); + commonToolDeleteService = module.get(CommonToolDeleteService); }); afterAll(async () => { @@ -98,43 +103,22 @@ describe(ContextExternalToolService.name, () => { }); }); - describe('deleteBySchoolExternalToolId', () => { - describe('when schoolExternalToolId is given', () => { + describe('deleteContextExternalTool', () => { + describe('when deleting a context external tool', () => { const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); - const schoolExternalToolId = schoolExternalTool.id; - const contextExternalTool1: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef(schoolExternalToolId) - .buildWithId(); - const contextExternalTool2: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef(schoolExternalToolId) - .buildWithId(); - contextExternalToolRepo.find.mockResolvedValueOnce([contextExternalTool1, contextExternalTool2]); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); return { - schoolExternalTool, - schoolExternalToolId, - contextExternalTool1, - contextExternalTool2, + contextExternalTool, }; }; - it('should call find()', async () => { - const { schoolExternalToolId } = setup(); - - await service.deleteBySchoolExternalToolId(schoolExternalToolId); - - expect(contextExternalToolRepo.find).toHaveBeenCalledWith({ - schoolToolRef: { schoolToolId: schoolExternalToolId }, - }); - }); - - it('should call deleteBySchoolExternalToolIds()', async () => { - const { schoolExternalToolId, contextExternalTool1, contextExternalTool2 } = setup(); + it('should delete the context external tool', async () => { + const { contextExternalTool } = setup(); - await service.deleteBySchoolExternalToolId(schoolExternalToolId); + await service.deleteContextExternalTool(contextExternalTool); - expect(contextExternalToolRepo.delete).toHaveBeenCalledWith([contextExternalTool1, contextExternalTool2]); + expect(commonToolDeleteService.deleteContextExternalTool).toHaveBeenCalledWith(contextExternalTool); }); }); }); @@ -244,26 +228,6 @@ describe(ContextExternalToolService.name, () => { }); }); - describe('deleteContextExternalTool', () => { - describe('when contextExternalToolId is given', () => { - const setup = () => { - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - - return { - contextExternalTool, - }; - }; - - it('should call delete on repo', async () => { - const { contextExternalTool } = setup(); - - await service.deleteContextExternalTool(contextExternalTool); - - expect(contextExternalToolRepo.delete).toHaveBeenCalledWith(contextExternalTool); - }); - }); - }); - describe('getContextExternalToolsForContext', () => { describe('when contextType and contextId are given', () => { it('should call the repository', async () => { diff --git a/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool.factory.ts index 88827f685c9..9d6140829cc 100644 --- a/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool.factory.ts +++ b/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool.factory.ts @@ -3,7 +3,8 @@ import { DoBaseFactory } from '@shared/testing/factory/domainobject/do-base.fact import { DeepPartial } from 'fishery'; import { CustomParameterEntry } from '../../common/domain'; import { ToolContextType } from '../../common/enum'; -import { ContextExternalTool, ContextExternalToolProps } from '../domain'; +import { SchoolExternalToolRef } from '../../school-external-tool/domain'; +import { ContextExternalTool, ContextExternalToolProps, ContextRef } from '../domain'; class ContextExternalToolFactory extends DoBaseFactory { withSchoolExternalToolRef(schoolToolId: string, schoolId?: string | undefined): this { @@ -24,8 +25,8 @@ class ContextExternalToolFactory extends DoBaseFactory { return { id: new ObjectId().toHexString(), - schoolToolRef: { schoolToolId: `schoolToolId-${sequence}`, schoolId: 'schoolId' }, - contextRef: { id: new ObjectId().toHexString(), type: ToolContextType.COURSE }, + schoolToolRef: new SchoolExternalToolRef({ schoolToolId: `schoolToolId-${sequence}`, schoolId: 'schoolId' }), + contextRef: new ContextRef({ id: new ObjectId().toHexString(), type: ToolContextType.COURSE }), displayName: 'My Course Tool 1', parameters: [new CustomParameterEntry({ name: 'param', value: 'value' })], }; 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 956827963ab..c613dc0275c 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,8 +10,7 @@ import { LegacyLogger } from '@src/core/logger'; import { OauthProviderService } from '../../../oauth-provider/domain/service/oauth-provider.service'; import { providerOauthClientFactory } from '../../../oauth-provider/testing'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { schoolExternalToolFactory } from '../../school-external-tool/testing'; +import { CommonToolDeleteService } from '../../common/service'; import { ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '../domain'; import { externalToolFactory, lti11ToolConfigFactory, oauth2ToolConfigFactory } from '../testing'; import { ExternalToolServiceMapper } from './external-tool-service.mapper'; @@ -25,6 +24,7 @@ describe(ExternalToolService.name, () => { let schoolToolRepo: DeepMocked; let courseToolRepo: DeepMocked; let oauthProviderService: DeepMocked; + let commonToolDeleteService: DeepMocked; let mapper: DeepMocked; let encryptionService: DeepMocked; @@ -60,6 +60,10 @@ describe(ExternalToolService.name, () => { provide: LegacyLogger, useValue: createMock(), }, + { + provide: CommonToolDeleteService, + useValue: createMock(), + }, ], }).compile(); @@ -69,6 +73,7 @@ describe(ExternalToolService.name, () => { courseToolRepo = module.get(ContextExternalToolRepo); oauthProviderService = module.get(OauthProviderService); mapper = module.get(ExternalToolServiceMapper); + commonToolDeleteService = module.get(CommonToolDeleteService); encryptionService = module.get(DefaultEncryptionService); }); @@ -375,40 +380,20 @@ describe(ExternalToolService.name, () => { describe('deleteExternalTool', () => { const setup = () => { - createTools(); - - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - - schoolToolRepo.findByExternalToolId.mockResolvedValue([schoolExternalTool]); + const externalTool = externalToolFactory.build(); return { - schoolExternalTool, + externalTool, }; }; - describe('when tool id is set', () => { - it('should delete all related CourseExternalTools', async () => { - const { schoolExternalTool } = setup(); - - await service.deleteExternalTool(schoolExternalTool.toolId); - - expect(courseToolRepo.deleteBySchoolExternalToolIds).toHaveBeenCalledWith([schoolExternalTool.id]); - }); - - it('should delete all related SchoolExternalTools', async () => { - const { schoolExternalTool } = setup(); - - await service.deleteExternalTool(schoolExternalTool.toolId); - - expect(schoolToolRepo.deleteByExternalToolId).toHaveBeenCalledWith(schoolExternalTool.toolId); - }); - - it('should delete the ExternalTool', async () => { - const { schoolExternalTool } = setup(); + describe('when deleting an external tool', () => { + it('should delete the external tool', async () => { + const { externalTool } = setup(); - await service.deleteExternalTool(schoolExternalTool.toolId); + await service.deleteExternalTool(externalTool); - expect(externalToolRepo.deleteById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(commonToolDeleteService.deleteExternalTool).toHaveBeenCalledWith(externalTool); }); }); }); diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts index 5909047186f..d4fcac26732 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts @@ -877,14 +877,17 @@ describe(ExternalToolUc.name, () => { const currentUser: ICurrentUser = { userId: 'userId' } as ICurrentUser; const user: User = userFactory.buildWithId(); const jwt = 'jwt'; + const externalTool = externalToolFactory.build(); authorizationService.getUserWithPermissions.mockResolvedValue(user); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { toolId, currentUser, user, jwt, + externalTool, }; }; @@ -898,11 +901,11 @@ describe(ExternalToolUc.name, () => { }); it('should call ExternalToolService', async () => { - const { toolId, currentUser } = setup(); + const { toolId, currentUser, externalTool } = setup(); await uc.deleteExternalTool(currentUser.userId, toolId, 'jwt'); - expect(externalToolService.deleteExternalTool).toHaveBeenCalledWith(toolId); + expect(externalToolService.deleteExternalTool).toHaveBeenCalledWith(externalTool); }); it('should call ExternalToolImageService', async () => { 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 9f3e50b6b9a..09433388abd 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 @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; import { SchoolExternalToolRepo } from '@shared/repo'; -import { CommonToolValidationService } from '../../common/service'; +import { CommonToolDeleteService, CommonToolValidationService } from '../../common/service'; import { ExternalToolService } from '../../external-tool'; import { type ExternalTool } from '../../external-tool/domain'; import { externalToolFactory } from '../../external-tool/testing'; @@ -18,6 +18,7 @@ describe(SchoolExternalToolService.name, () => { let schoolExternalToolRepo: DeepMocked; let externalToolService: DeepMocked; let commonToolValidationService: DeepMocked; + let commonToolDeleteService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -35,6 +36,10 @@ describe(SchoolExternalToolService.name, () => { provide: CommonToolValidationService, useValue: createMock(), }, + { + provide: CommonToolDeleteService, + useValue: createMock(), + }, ], }).compile(); @@ -42,6 +47,7 @@ describe(SchoolExternalToolService.name, () => { schoolExternalToolRepo = module.get(SchoolExternalToolRepo); externalToolService = module.get(ExternalToolService); commonToolValidationService = module.get(CommonToolValidationService); + commonToolDeleteService = module.get(CommonToolDeleteService); }); describe('findSchoolExternalTools', () => { @@ -101,26 +107,22 @@ describe(SchoolExternalToolService.name, () => { }); }); - describe('deleteSchoolExternalToolById', () => { - describe('when schoolExternalToolId is given', () => { + describe('deleteSchoolExternalTool', () => { + 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); return { - schoolExternalToolId: schoolExternalTool.id, + schoolExternalTool, }; }; - it('should call the schoolExternalToolRepo', () => { - const { schoolExternalToolId } = setup(); + it('should call the schoolExternalToolRepo', async () => { + const { schoolExternalTool } = setup(); - service.deleteSchoolExternalToolById(schoolExternalToolId); + await service.deleteSchoolExternalTool(schoolExternalTool); - expect(schoolExternalToolRepo.deleteById).toHaveBeenCalledWith(schoolExternalToolId); + expect(commonToolDeleteService.deleteSchoolExternalTool).toHaveBeenCalledWith(schoolExternalTool); }); }); }); diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts index 1648171c5ac..9305afbefef 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts @@ -8,7 +8,6 @@ import { Permission } from '@shared/domain/interface'; import { setupEntities, userFactory } from '@shared/testing'; import { School, SchoolService } from '@src/modules/school'; import { CommonToolMetadataService } from '../../common/service/common-tool-metadata.service'; -import { ContextExternalToolService } from '../../context-external-tool'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; import { schoolExternalToolFactory } from '../testing'; @@ -20,7 +19,6 @@ describe('SchoolExternalToolUc', () => { let uc: SchoolExternalToolUc; let schoolExternalToolService: DeepMocked; - let contextExternalToolService: DeepMocked; let schoolExternalToolValidationService: DeepMocked; let commonToolMetadataService: DeepMocked; let authorizationService: DeepMocked; @@ -35,10 +33,6 @@ describe('SchoolExternalToolUc', () => { provide: SchoolExternalToolService, useValue: createMock(), }, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, { provide: SchoolExternalToolValidationService, useValue: createMock(), @@ -60,7 +54,6 @@ describe('SchoolExternalToolUc', () => { uc = module.get(SchoolExternalToolUc); schoolExternalToolService = module.get(SchoolExternalToolService); - contextExternalToolService = module.get(ContextExternalToolService); schoolExternalToolValidationService = module.get(SchoolExternalToolValidationService); commonToolMetadataService = module.get(CommonToolMetadataService); authorizationService = module.get(AuthorizationService); From 1d7f126f67dcf1533dd317fe8f4cc9dfc1685d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 16 Aug 2024 16:04:07 +0200 Subject: [PATCH 4/8] fix test --- .../service/external-tool.service.spec.ts | 14 +------------- .../external-tool/service/external-tool.service.ts | 4 +--- 2 files changed, 2 insertions(+), 16 deletions(-) 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 c613dc0275c..ea54061dd37 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 @@ -5,7 +5,7 @@ import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Page } from '@shared/domain/domainobject'; import { IFindOptions, SortOrder } from '@shared/domain/interface'; -import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { ExternalToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { OauthProviderService } from '../../../oauth-provider/domain/service/oauth-provider.service'; import { providerOauthClientFactory } from '../../../oauth-provider/testing'; @@ -21,8 +21,6 @@ describe(ExternalToolService.name, () => { let service: ExternalToolService; let externalToolRepo: DeepMocked; - let schoolToolRepo: DeepMocked; - let courseToolRepo: DeepMocked; let oauthProviderService: DeepMocked; let commonToolDeleteService: DeepMocked; let mapper: DeepMocked; @@ -48,14 +46,6 @@ describe(ExternalToolService.name, () => { provide: DefaultEncryptionService, useValue: createMock(), }, - { - provide: SchoolExternalToolRepo, - useValue: createMock(), - }, - { - provide: ContextExternalToolRepo, - useValue: createMock(), - }, { provide: LegacyLogger, useValue: createMock(), @@ -69,8 +59,6 @@ describe(ExternalToolService.name, () => { service = module.get(ExternalToolService); externalToolRepo = module.get(ExternalToolRepo); - schoolToolRepo = module.get(SchoolExternalToolRepo); - courseToolRepo = module.get(ContextExternalToolRepo); oauthProviderService = module.get(OauthProviderService); mapper = module.get(ExternalToolServiceMapper); commonToolDeleteService = module.get(CommonToolDeleteService); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index 5d8a9e2a502..2af557e4204 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { ExternalToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { TokenEndpointAuthMethod } from '../../common/enum'; import { ExternalToolSearchQuery } from '../../common/interface'; @@ -19,8 +19,6 @@ export class ExternalToolService { private readonly externalToolRepo: ExternalToolRepo, private readonly oauthProviderService: OauthProviderService, private readonly mapper: ExternalToolServiceMapper, - private readonly schoolExternalToolRepo: SchoolExternalToolRepo, - private readonly contextExternalToolRepo: ContextExternalToolRepo, @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, private readonly legacyLogger: LegacyLogger, private readonly commonToolDeleteService: CommonToolDeleteService From a631eb9e1a5bf09ae2636b64f44592f5a61ce876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 16 Aug 2024 16:17:08 +0200 Subject: [PATCH 5/8] fix test --- .../external-tool/controller/api-test/tool.api.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 6a646b2d5b6..c0242b68070 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 @@ -714,21 +714,21 @@ describe('ToolController (API)', () => { describe('when permission is missing', () => { const setup = async () => { - const toolId: string = new ObjectId().toHexString(); + const externalTool = externalToolEntityFactory.build(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin(); - await em.persistAndFlush([adminAccount, adminUser]); + await em.persistAndFlush([adminAccount, adminUser, externalTool]); em.clear(); const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); - return { loggedInClient, toolId }; + return { loggedInClient, externalTool }; }; it('should return unauthorized', async () => { - const { loggedInClient, toolId } = await setup(); + const { loggedInClient, externalTool } = await setup(); - const response: Response = await loggedInClient.delete(`${toolId}`); + const response: Response = await loggedInClient.delete(externalTool.id); expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); }); From e15b87283ad5bac5f0d0e6a898b84eebc150040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Mon, 19 Aug 2024 14:26:27 +0200 Subject: [PATCH 6/8] cypress test data --- backup/setup/boardnodes.json | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 backup/setup/boardnodes.json diff --git a/backup/setup/boardnodes.json b/backup/setup/boardnodes.json new file mode 100644 index 00000000000..af2c1dea0f7 --- /dev/null +++ b/backup/setup/boardnodes.json @@ -0,0 +1,74 @@ +[ + { + "_id": { + "$oid": "649452a81fef6fd796117f7f" + }, + "context": { + "$oid": "5fa3a2f3a9c31a26f4d1d309" + }, + "contextType": "course", + "createdAt": { + "$date": "2023-06-22T13:54:48.996Z" + }, + "isVisible": true, + "layout": "columns", + "level": 0, + "path": ",", + "position": 0, + "title": "CY Kurs-Board", + "type": "column-board", + "updatedAt": { + "$date": "2023-06-22T13:54:48.996Z" + } + }, + { + "_id": { + "$oid": "649452a91fef6fd796117f80" + }, + "createdAt": { + "$date": "2024-05-31T09:01:06.497Z" + }, + "level": 1, + "path": ",649452a81fef6fd796117f7f,", + "position": 0, + "title": "CY Column", + "type": "column", + "updatedAt": { + "$date": "2024-05-31T09:01:06.508Z" + } + }, + { + "_id": { + "$oid": "649452a91fef6fd796117f81" + }, + "createdAt": { + "$date": "2024-05-31T09:01:06.500Z" + }, + "level": 2, + "path": ",649452a81fef6fd796117f7f,649452a91fef6fd796117f80,", + "position": 0, + "title": "CY Card", + "type": "card", + "updatedAt": { + "$date": "2024-05-31T09:01:06.508Z" + }, + "height": 254.203125 + }, + { + "_id": { + "$oid": "649457451fef6fd796117f8a" + }, + "createdAt": { + "$date": "2023-06-22T14:14:29.082Z" + }, + "level": 3, + "path": ",649452a81fef6fd796117f7f,649452a91fef6fd796117f80,649452a91fef6fd796117f81,", + "position": 0, + "type": "deleted-element", + "updatedAt": { + "$date": "2023-06-22T14:14:29.083Z" + }, + "title": "CY Deleted Tool", + "deletedElementType": "externalTool" + } +] \ No newline at end of file From 44249e01504c509896c135ed4106ec8eede56432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Tue, 20 Aug 2024 08:39:35 +0200 Subject: [PATCH 7/8] add tests --- .../board/domain/deleted-element.do.spec.ts | 53 +++++++++++++++++++ .../board/domain/deleted-element.do.ts | 4 ++ .../board-node-copy-specific.service.spec.ts | 22 ++++++++ 3 files changed, 79 insertions(+) create mode 100644 apps/server/src/modules/board/domain/deleted-element.do.spec.ts diff --git a/apps/server/src/modules/board/domain/deleted-element.do.spec.ts b/apps/server/src/modules/board/domain/deleted-element.do.spec.ts new file mode 100644 index 00000000000..a0f6aa9a061 --- /dev/null +++ b/apps/server/src/modules/board/domain/deleted-element.do.spec.ts @@ -0,0 +1,53 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletedElement } from './deleted-element.do'; +import { BoardNodeProps, ContentElementType } from './types'; + +describe(DeletedElement.name, () => { + let element: DeletedElement; + + const boardNodeProps: BoardNodeProps = { + id: new ObjectId().toHexString(), + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + element = new DeletedElement({ + ...boardNodeProps, + deletedElementType: ContentElementType.EXTERNAL_TOOL, + title: 'Old Tool', + }); + }); + + it('should return title', () => { + expect(element.title).toEqual('Old Tool'); + }); + + it('should set title', () => { + const title = 'Title'; + + element.title = title; + + expect(element.title).toEqual(title); + }); + + it('should return deletedElementType', () => { + expect(element.deletedElementType).toEqual(ContentElementType.EXTERNAL_TOOL); + }); + + it('should set deletedElementType', () => { + const deletedElementType = ContentElementType.FILE; + + element.deletedElementType = deletedElementType; + + expect(element.deletedElementType).toEqual(deletedElementType); + }); + + it('should not have child', () => { + expect(element.canHaveChild()).toEqual(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/deleted-element.do.ts b/apps/server/src/modules/board/domain/deleted-element.do.ts index ce923872bc1..b274e0a32f1 100644 --- a/apps/server/src/modules/board/domain/deleted-element.do.ts +++ b/apps/server/src/modules/board/domain/deleted-element.do.ts @@ -14,6 +14,10 @@ export class DeletedElement extends BoardNode { return this.props.deletedElementType; } + set deletedElementType(value: ContentElementType) { + this.props.deletedElementType = value; + } + canHaveChild(): boolean { return false; } 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 87e09b13a41..66bd8b6ea0d 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 @@ -15,6 +15,7 @@ import { CollaborativeTextEditorElement, Column, ColumnBoard, + DeletedElement, DrawingElement, ExternalToolElement, FileElement, @@ -27,6 +28,7 @@ import { collaborativeTextEditorFactory, columnBoardFactory, columnFactory, + deletedElementFactory, drawingElementFactory, externalToolElementFactory, fileElementFactory, @@ -604,4 +606,24 @@ describe(BoardNodeCopyService.name, () => { ); }); }); + + describe('copy deleted element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const deletedElement = deletedElementFactory.build(); + + return { + copyContext, + deletedElement, + }; + }; + + it('should copy the node', async () => { + const { copyContext, deletedElement } = setup(); + + const result = await service.copyDeletedElement(deletedElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(DeletedElement); + }); + }); }); From d9bf147298b958cd2f61a5a8dfc6f02b4331bcca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:45:16 +0200 Subject: [PATCH 8/8] Update board-node.service.spec.ts --- .../server/src/modules/board/service/board-node.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/service/board-node.service.spec.ts b/apps/server/src/modules/board/service/board-node.service.spec.ts index 668b1e883a6..90e2cd7fb3c 100644 --- a/apps/server/src/modules/board/service/board-node.service.spec.ts +++ b/apps/server/src/modules/board/service/board-node.service.spec.ts @@ -212,7 +212,7 @@ describe(BoardNodeService.name, () => { }); describe('findElementsByContextExternalToolId', () => { - describe('when finding a node by its ', () => { + describe('when finding a node by its context external tool id', () => { const setup = () => { const contextExternalToolId = new ObjectId().toHexString(); const node = externalToolElementFactory.build({