From 73595f420c442eaa5469b3603aeeb91d0b007881 Mon Sep 17 00:00:00 2001 From: virgilchiriac Date: Tue, 24 Oct 2023 02:06:26 +0200 Subject: [PATCH] add tests --- .../content-element-create.api.spec.ts | 180 ++++++++++++------ .../service/content-element.service.spec.ts | 37 ++++ .../src/modules/board/uc/element.uc.spec.ts | 70 ++++++- .../board/uc/submission-item.uc.spec.ts | 37 +++- 4 files changed, 248 insertions(+), 76 deletions(-) diff --git a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts index 57ef692ace1..a021fcc477f 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts @@ -37,101 +37,161 @@ describe(`content element create (api)`, () => { }); describe('with valid user', () => { - const setup = async () => { - await cleanupCollections(em); + describe('when parent is a card', () => { + const setup = async () => { + await cleanupCollections(em); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const course = courseFactory.build({ teachers: [teacherUser] }); - await em.persistAndFlush([teacherAccount, teacherUser, course]); + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ - context: { id: course.id, type: BoardExternalReferenceType.Course }, + const columnBoardNode = columnBoardNodeFactory.buildWithId({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + + await em.persistAndFlush([columnBoardNode, columnNode, cardNode]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, columnBoardNode, columnNode, cardNode }; + }; + + it('should return status 201', async () => { + const { loggedInClient, cardNode } = await setup(); + + const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.RICH_TEXT }); + + expect(response.statusCode).toEqual(201); }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - await em.persistAndFlush([columnBoardNode, columnNode, cardNode]); - em.clear(); + it('should return the created content element of type RICH_TEXT', async () => { + const { loggedInClient, cardNode } = await setup(); - const loggedInClient = await testApiClient.login(teacherAccount); + const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.RICH_TEXT }); - return { loggedInClient, columnBoardNode, columnNode, cardNode }; - }; + expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.RICH_TEXT); + }); - it('should return status 201', async () => { - const { loggedInClient, cardNode } = await setup(); + it('should return the created content element of type FILE', async () => { + const { loggedInClient, cardNode } = await setup(); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.RICH_TEXT }); + const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.FILE }); - expect(response.statusCode).toEqual(201); - }); + expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.FILE); + }); - it('should return the created content element of type RICH_TEXT', async () => { - const { loggedInClient, cardNode } = await setup(); + it('should return the created content element of type EXTERNAL_TOOL', async () => { + const { loggedInClient, cardNode } = await setup(); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.RICH_TEXT }); + const response = await loggedInClient.post(`${cardNode.id}/elements`, { + type: ContentElementType.EXTERNAL_TOOL, + }); - expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.RICH_TEXT); - }); + expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.EXTERNAL_TOOL); + }); - it('should return the created content element of type FILE', async () => { - const { loggedInClient, cardNode } = await setup(); + it('should return the created content element of type SUBMISSION_CONTAINER with dueDate set to null', async () => { + const { loggedInClient, cardNode } = await setup(); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.FILE }); + const response = await loggedInClient.post(`${cardNode.id}/elements`, { + type: ContentElementType.SUBMISSION_CONTAINER, + }); - expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.FILE); - }); + expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.SUBMISSION_CONTAINER); + expect((response.body as SubmissionContainerElementResponse).content.dueDate).toBeNull(); + }); - it('should return the created content element of type EXTERNAL_TOOL', async () => { - const { loggedInClient, cardNode } = await setup(); + it('should actually create the content element', async () => { + const { loggedInClient, cardNode } = await setup(); + const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.RICH_TEXT }); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.EXTERNAL_TOOL }); + const elementId = (response.body as AnyContentElementResponse).id; - expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.EXTERNAL_TOOL); - }); + const result = await em.findOneOrFail(RichTextElementNode, elementId); + expect(result.id).toEqual(elementId); + }); + + it('should throw an error if toPosition param is not a number', async () => { + const { loggedInClient, cardNode } = await setup(); - it('should return the created content element of type SUBMISSION_CONTAINER with dueDate set to null', async () => { - const { loggedInClient, cardNode } = await setup(); + const response = await loggedInClient.post(`${cardNode.id}/elements`, { + type: ContentElementType.RICH_TEXT, + toPosition: 'not a number', + }); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { - type: ContentElementType.SUBMISSION_CONTAINER, + expect(response.statusCode).toEqual(400); }); - expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.SUBMISSION_CONTAINER); - expect((response.body as SubmissionContainerElementResponse).content.dueDate).toBeNull(); + it('should throw an error if toPosition param is a negative number', async () => { + const { loggedInClient, cardNode } = await setup(); + + const response = await loggedInClient.post(`${cardNode.id}/elements`, { + type: ContentElementType.RICH_TEXT, + toPosition: -1, + }); + + expect(response.statusCode).toEqual(400); + }); }); - it('should actually create the content element', async () => { - const { loggedInClient, cardNode } = await setup(); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { type: ContentElementType.RICH_TEXT }); + describe('when parent is a submission item', () => { + const setup = async () => { + await cleanupCollections(em); + + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const elementId = (response.body as AnyContentElementResponse).id; + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherAccount, teacherUser, course]); - const result = await em.findOneOrFail(RichTextElementNode, elementId); - expect(result.id).toEqual(elementId); - }); + const columnBoardNode = columnBoardNodeFactory.buildWithId({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const submissionContainerNode = columnNodeFactory.buildWithId({ parent: cardNode }); + const submissionItemNode = cardNodeFactory.buildWithId({ parent: submissionContainerNode }); + + await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, cardNode, submissionItemNode }; + }; - it('should throw an error if toPosition param is not a number', async () => { - const { loggedInClient, cardNode } = await setup(); + it('should return status 201', async () => { + const { loggedInClient, submissionItemNode } = await setup(); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { - type: ContentElementType.RICH_TEXT, - toPosition: 'not a number', + const response = await loggedInClient.post(`${submissionItemNode.id}/elements`, { + type: ContentElementType.RICH_TEXT, + }); + + expect(response.statusCode).toEqual(201); }); - expect(response.statusCode).toEqual(400); - }); + it('should return the created content element of type RICH_TEXT', async () => { + const { loggedInClient, submissionItemNode } = await setup(); - it('should throw an error if toPosition param is a negative number', async () => { - const { loggedInClient, cardNode } = await setup(); + const response = await loggedInClient.post(`${submissionItemNode.id}/elements`, { + type: ContentElementType.RICH_TEXT, + }); - const response = await loggedInClient.post(`${cardNode.id}/elements`, { - type: ContentElementType.RICH_TEXT, - toPosition: -1, + expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.RICH_TEXT); }); - expect(response.statusCode).toEqual(400); + it('should return the created content element of type FILE', async () => { + const { loggedInClient, submissionItemNode } = await setup(); + + const response = await loggedInClient.post(`${submissionItemNode.id}/elements`, { + type: ContentElementType.FILE, + }); + + expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.FILE); + }); }); }); diff --git a/apps/server/src/modules/board/service/content-element.service.spec.ts b/apps/server/src/modules/board/service/content-element.service.spec.ts index b1326450089..90f3a73aa21 100644 --- a/apps/server/src/modules/board/service/content-element.service.spec.ts +++ b/apps/server/src/modules/board/service/content-element.service.spec.ts @@ -123,6 +123,43 @@ describe(ContentElementService.name, () => { }); }); + describe('findParentOfId', () => { + describe('when parent is a vaid node', () => { + const setup = () => { + const card = cardFactory.build(); + const element = richTextElementFactory.build(); + + return { element, card }; + }; + + it('should call the repo', async () => { + const { element, card } = setup(); + boardDoRepo.findParentOfId.mockResolvedValueOnce(card); + + await service.findParentOfId(element.id); + + expect(boardDoRepo.findParentOfId).toHaveBeenCalledWith(element.id); + }); + + it('should throw NotFoundException', async () => { + const { element } = setup(); + + boardDoRepo.findParentOfId.mockResolvedValue(undefined); + + await expect(service.findParentOfId(element.id)).rejects.toThrowError(NotFoundException); + }); + + it('should return the parent', async () => { + const { element, card } = setup(); + boardDoRepo.findParentOfId.mockResolvedValueOnce(card); + + const result = await service.findParentOfId(element.id); + + expect(result).toEqual(card); + }); + }); + }); + describe('create', () => { describe('when creating a content element of type', () => { const setup = () => { diff --git a/apps/server/src/modules/board/uc/element.uc.spec.ts b/apps/server/src/modules/board/uc/element.uc.spec.ts index 8945b9612e8..e17c20bb067 100644 --- a/apps/server/src/modules/board/uc/element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/element.uc.spec.ts @@ -10,8 +10,9 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationService } from '@modules/authorization'; +import { AuthorizationService, Action } from '@modules/authorization'; import { ObjectId } from 'bson'; +import { ForbiddenException } from '@nestjs/common'; import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; import { ElementUc } from './element.uc'; @@ -123,6 +124,61 @@ describe(ElementUc.name, () => { }); describe('deleteElement', () => { + describe('when deleting an element which has a submission item parent', () => { + const setup = () => { + const user = userFactory.build(); + const element = richTextElementFactory.build(); + const submissionItem = submissionItemFactory.build({ userId: user.id }); + + boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( + new BoardDoAuthorizable({ users: [], id: new ObjectId().toHexString() }) + ); + + elementService.findById.mockResolvedValueOnce(element); + return { element, user, submissionItem }; + }; + + it('should call the service to find the element', async () => { + const { element, user } = setup(); + await uc.deleteElement(user.id, element.id); + + expect(elementService.findById).toHaveBeenCalledWith(element.id); + }); + + it('should call the service to find the parent of the element', async () => { + const { element, user } = setup(); + await uc.deleteElement(user.id, element.id); + + expect(elementService.findParentOfId).toHaveBeenCalledWith(element.id); + }); + + it('should throw if the user is not the owner of the submission item', async () => { + const { element, user } = setup(); + const otherSubmissionItem = submissionItemFactory.build({ userId: new ObjectId().toHexString() }); + elementService.findParentOfId.mockResolvedValueOnce(otherSubmissionItem); + + await expect(uc.deleteElement(user.id, element.id)).rejects.toThrow(new ForbiddenException()); + }); + + it('should authorize the user to delete the element', async () => { + const { element, user, submissionItem } = setup(); + elementService.findParentOfId.mockResolvedValueOnce(submissionItem); + const boardDoAuthorizable = await boardDoAuthorizableService.getBoardAuthorizable(submissionItem); + const context = { action: Action.read, requiredPermissions: [] }; + await uc.deleteElement(user.id, element.id); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, boardDoAuthorizable, context); + }); + + it('should call the service to delete the element', async () => { + const { user, element, submissionItem } = setup(); + elementService.findParentOfId.mockResolvedValueOnce(submissionItem); + + await uc.deleteElement(user.id, element.id); + + expect(elementService.delete).toHaveBeenCalledWith(element); + }); + }); describe('when deleting a content element', () => { const setup = () => { const user = userFactory.build(); @@ -160,9 +216,9 @@ describe(ElementUc.name, () => { const user = userFactory.build(); const fileElement = fileElementFactory.build(); - const elementSpy = elementService.findById.mockResolvedValue(fileElement); + elementService.findById.mockResolvedValue(fileElement); - return { fileElement, user, elementSpy }; + return { fileElement, user }; }; it('should throw', async () => { @@ -181,9 +237,9 @@ describe(ElementUc.name, () => { const submissionContainer = submissionContainerElementFactory.build({ children: [fileElement] }); - const elementSpy = elementService.findById.mockResolvedValue(submissionContainer); + elementService.findById.mockResolvedValue(submissionContainer); - return { submissionContainer, fileElement, user, elementSpy }; + return { submissionContainer, fileElement, user }; }; it('should throw', async () => { @@ -202,9 +258,9 @@ describe(ElementUc.name, () => { const submissionItem = submissionItemFactory.build({ userId: user.id }); const submissionContainer = submissionContainerElementFactory.build({ children: [submissionItem] }); - const elementSpy = elementService.findById.mockResolvedValue(submissionContainer); + elementService.findById.mockResolvedValue(submissionContainer); - return { submissionContainer, submissionItem, user, elementSpy }; + return { submissionContainer, submissionItem, user }; }; it('should throw', async () => { diff --git a/apps/server/src/modules/board/uc/submission-item.uc.spec.ts b/apps/server/src/modules/board/uc/submission-item.uc.spec.ts index 122caae2bf6..93fa11fd48b 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.spec.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.spec.ts @@ -11,10 +11,14 @@ import { } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { Action, AuthorizationService } from '@modules/authorization'; -import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + ForbiddenException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; import { SubmissionItemUc } from './submission-item.uc'; -import { isFileElementResponse, isRichTextElementResponse } from '../controller/dto'; describe(SubmissionItemUc.name, () => { let module: TestingModule; @@ -263,7 +267,6 @@ describe(SubmissionItemUc.name, () => { submissionItemService.findById.mockResolvedValue(submissionItem); const element = richTextElementFactory.build(); - elementService.create.mockResolvedValueOnce(element); boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( new BoardDoAuthorizable({ @@ -272,17 +275,20 @@ describe(SubmissionItemUc.name, () => { }) ); - return { submissionItem, user }; + return { element, submissionItem, user }; }; it('should call service to find the submission item ', async () => { - const { submissionItem, user } = setup(); + const { element, submissionItem, user } = setup(); + elementService.create.mockResolvedValueOnce(element); + await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); expect(submissionItemService.findById).toHaveBeenCalledWith(submissionItem.id); }); it('should authorize', async () => { - const { submissionItem, user } = setup(); + const { element, submissionItem, user } = setup(); + elementService.create.mockResolvedValueOnce(element); const boardDoAuthorizable = await boardDoAuthorizableService.getBoardAuthorizable(submissionItem); @@ -308,15 +314,28 @@ describe(SubmissionItemUc.name, () => { }); it('should call service to create element', async () => { - const { submissionItem, user } = setup(); + const { element, submissionItem, user } = setup(); + elementService.create.mockResolvedValueOnce(element); + await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); expect(elementService.create).toHaveBeenCalledWith(submissionItem, ContentElementType.RICH_TEXT); }); it('should return element', async () => { + const { element, submissionItem, user } = setup(); + elementService.create.mockResolvedValueOnce(element); + + const returnedElement = await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); + expect(returnedElement).toEqual(element); + }); + + it('should throw if element is not file or rich text', async () => { const { submissionItem, user } = setup(); - const element = await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); - expect(element).toBeDefined(); + const otherElement = submissionContainerElementFactory.build(); + elementService.create.mockResolvedValueOnce(otherElement); + await expect(uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT)).rejects.toThrow( + new UnprocessableEntityException() + ); }); }); });