diff --git a/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts b/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts index 573af83d04f..2ead06c804d 100644 --- a/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts +++ b/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts @@ -5,6 +5,6 @@ import { BoardLayout } from '../../../domain/types'; export class LayoutBodyParams { @IsEnum(BoardLayout) @NotEquals(BoardLayout[BoardLayout.COLUMNS]) - @ApiProperty({ enum: BoardLayout, enumName: 'BoardLayout' }) + @ApiProperty({ enum: BoardLayout, enumName: 'MediaBoardLayoutType' }) layout!: BoardLayout; } diff --git a/apps/server/src/modules/board/domain/type-mapping.spec.ts b/apps/server/src/modules/board/domain/type-mapping.spec.ts index 73142151f5f..55c1917f681 100644 --- a/apps/server/src/modules/board/domain/type-mapping.spec.ts +++ b/apps/server/src/modules/board/domain/type-mapping.spec.ts @@ -1,4 +1,4 @@ -import { getBoardNodeType } from './type-mapping'; +import { getBoardNodeType, handleNonExhaustiveSwitch } from './type-mapping'; import { BoardNodeType } from './types/board-node-type.enum'; @@ -44,3 +44,9 @@ describe('getBoardNodeType', () => { expect(() => getBoardNodeType(new UnknownType() as any)).toThrow(); }); }); + +describe('handleNonExhaustiveSwitch', () => { + it('should throw error', () => { + expect(() => handleNonExhaustiveSwitch('unknown' as never)).toThrow(); + }); +}); diff --git a/apps/server/src/modules/board/service/board-node-permission.service.spec.ts b/apps/server/src/modules/board/service/board-node-permission.service.spec.ts index 4af303e97e1..7ddb77902fa 100644 --- a/apps/server/src/modules/board/service/board-node-permission.service.spec.ts +++ b/apps/server/src/modules/board/service/board-node-permission.service.spec.ts @@ -118,6 +118,18 @@ describe(BoardNodePermissionService.name, () => { const result = service.isUserBoardEditor(user.id, boardDoAuthorizable.users); expect(result).toBe(false); }); + + it('should return false if user is not part of board', () => { + const { anyBoardDo, user } = setup(); + const boardDoAuthorizable = new BoardNodeAuthorizable({ + users: [], + id: anyBoardDo.id, + boardNode: anyBoardDo, + rootNode: anyBoardDo, + }); + const result = service.isUserBoardEditor(user.id, boardDoAuthorizable.users); + expect(result).toBe(false); + }); }); describe('isUserBoardReader', () => { diff --git a/apps/server/src/modules/board/service/column-board.service.spec.ts b/apps/server/src/modules/board/service/column-board.service.spec.ts new file mode 100644 index 00000000000..824d3876747 --- /dev/null +++ b/apps/server/src/modules/board/service/column-board.service.spec.ts @@ -0,0 +1,125 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EntityId } from '@shared/domain/types'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ColumnBoardService } from './column-board.service'; +import { BoardNodeRepo } from '../repo'; +import { BoardNodeService } from './board-node.service'; +import { ColumnBoardCopyService, ColumnBoardLinkService } from './internal'; +import { ColumnBoard, BoardExternalReference, BoardExternalReferenceType } from '../domain'; + +import { columnBoardFactory } from '../testing'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; + +describe('ColumnBoardService', () => { + let service: ColumnBoardService; + let repo: jest.Mocked; + let boardNodeService: jest.Mocked; + let columnBoardCopyService: DeepMocked; + let columnBoardLinkService: DeepMocked; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ColumnBoardService, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + { + provide: BoardNodeService, + useValue: createMock(), + }, + { + provide: ColumnBoardCopyService, + useValue: createMock(), + }, + { + provide: ColumnBoardLinkService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ColumnBoardService); + repo = module.get(BoardNodeRepo); + boardNodeService = module.get(BoardNodeService); + columnBoardCopyService = module.get(ColumnBoardCopyService); + columnBoardLinkService = module.get(ColumnBoardLinkService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should find ColumnBoard by id', async () => { + const columnBoard = columnBoardFactory.build(); + boardNodeService.findByClassAndId.mockResolvedValue(columnBoard); + + const result = await service.findById('1'); + + expect(result).toBe(columnBoard); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, '1', undefined); + }); + + it('should find ColumnBoards by external reference', async () => { + const columnBoard = columnBoardFactory.build(); + repo.findByExternalReference.mockResolvedValue([columnBoard]); + const reference: BoardExternalReference = { + type: BoardExternalReferenceType.Course, + id: '1', + }; + + const result = await service.findByExternalReference(reference); + + expect(result).toEqual([columnBoard]); + expect(repo.findByExternalReference).toHaveBeenCalledWith(reference, undefined); + }); + + it('should update ColumnBoard visibility', async () => { + const columnBoard = columnBoardFactory.build(); + + await service.updateVisibility(columnBoard, true); + + expect(boardNodeService.updateVisibility).toHaveBeenCalledWith(columnBoard, true); + }); + + it('should delete ColumnBoards by course id', async () => { + const columnBoard = columnBoardFactory.build(); + repo.findByExternalReference.mockResolvedValue([columnBoard]); + const reference: BoardExternalReference = { + type: BoardExternalReferenceType.Course, + id: '1', + }; + + await service.deleteByCourseId('1'); + + expect(repo.findByExternalReference).toHaveBeenCalledWith(reference, undefined); + expect(repo.delete).toHaveBeenCalledWith([columnBoard]); + }); + + it('should copy ColumnBoard', async () => { + const copyStatus: CopyStatus = { status: CopyStatusEnum.SUCCESS, type: CopyElementType.COLUMNBOARD }; + columnBoardCopyService.copyColumnBoard.mockResolvedValue(copyStatus); + const result = await service.copyColumnBoard({ + originalColumnBoardId: '1', + destinationExternalReference: { + type: BoardExternalReferenceType.Course, + id: '1', + }, + userId: '1', + }); + + expect(result).toEqual(copyStatus); + }); + + it('should swap Linked Ids', async () => { + const idMap = new Map(); + idMap.set('1', '2'); + const columnBoard = columnBoardFactory.build(); + columnBoardLinkService.swapLinkedIds.mockResolvedValue(columnBoard); + + const result = await service.swapLinkedIds('1', idMap); + + expect(result).toEqual(columnBoard); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts b/apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts new file mode 100644 index 00000000000..e65225b37d3 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts @@ -0,0 +1,135 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InputFormat } from '@shared/domain/types'; +import { ContentElementUpdateService } from './content-element-update.service'; +import { + FileContentBody, + LinkContentBody, + RichTextContentBody, + DrawingContentBody, + SubmissionContainerContentBody, + ExternalToolContentBody, +} from '../../controller/dto'; +import { BoardNodeRepo } from '../../repo'; + +import { + drawingElementFactory, + externalToolElementFactory, + fileElementFactory, + linkElementFactory, + richTextElementFactory, + submissionContainerElementFactory, +} from '../../testing'; + +describe('ContentElementUpdateService', () => { + let module: TestingModule; + let service: ContentElementUpdateService; + let repo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ContentElementUpdateService, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ContentElementUpdateService); + repo = module.get(BoardNodeRepo); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should update FileElement', async () => { + const element = fileElementFactory.build(); + const content = new FileContentBody(); + content.caption = 'caption'; + content.alternativeText = 'alternativeText'; + + await service.updateContent(element, content); + + expect(element.caption).toBe('caption'); + expect(element.alternativeText).toBe('alternativeText'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update LinkElement', async () => { + const element = linkElementFactory.build(); + const content = new LinkContentBody(); + content.url = 'http://example.com/'; + content.title = 'title'; + content.description = 'description'; + content.imageUrl = 'relative-image.jpg'; + + await service.updateContent(element, content); + + expect(element.url).toBe('http://example.com/'); + expect(element.title).toBe('title'); + expect(element.description).toBe('description'); + expect(element.imageUrl).toBe('relative-image.jpg'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update RichTextElement', async () => { + const element = richTextElementFactory.build(); + const content = new RichTextContentBody(); + content.text = 'text'; + content.inputFormat = InputFormat.PLAIN_TEXT; + + await service.updateContent(element, content); + + expect(element.text).toBe('text'); + expect(element.inputFormat).toBe(InputFormat.PLAIN_TEXT); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update DrawingElement', async () => { + const element = drawingElementFactory.build(); + const content = new DrawingContentBody(); + content.description = 'description'; + + await service.updateContent(element, content); + + expect(element.description).toBe('description'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update SubmissionContainerElement', async () => { + const element = submissionContainerElementFactory.build(); + const content = new SubmissionContainerContentBody(); + content.dueDate = new Date(); + + await service.updateContent(element, content); + + expect(element.dueDate).toEqual(content.dueDate); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update ExternalToolElement', async () => { + const element = externalToolElementFactory.build(); + const content = new ExternalToolContentBody(); + content.contextExternalToolId = 'contextExternalToolId'; + + await service.updateContent(element, content); + + expect(element.contextExternalToolId).toBe('contextExternalToolId'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should throw error for unknown element type', async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const element = {} as any; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const content = {} as any; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await expect(service.updateContent(element, content)).rejects.toThrowError( + "Cannot update element of type: 'Object'" + ); + }); +});