Skip to content

Commit

Permalink
BC-5102 - use type guards (#4450)
Browse files Browse the repository at this point in the history
  • Loading branch information
virgilchiriac authored Oct 2, 2023
1 parent 6fee998 commit 13f4d7a
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { NotFoundException } from '@nestjs/common';
import { NotFoundException, UnprocessableEntityException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { SubmissionItem } from '@shared/domain';
import { ValidationError } from '@shared/common';
import { setupEntities, userFactory } from '@shared/testing';
import { richTextElementFactory, setupEntities, userFactory } from '@shared/testing';
import {
cardFactory,
submissionContainerElementFactory,
Expand Down Expand Up @@ -108,14 +108,22 @@ describe(SubmissionItemService.name, () => {
return { submissionContainer, submissionItem };
};

it('should fetch the SubmissionContainer parent', async () => {
it('should fetch the parent', async () => {
const { submissionItem } = setup();

await service.update(submissionItem, true);

expect(boardDoRepo.findParentOfId).toHaveBeenCalledWith(submissionItem.id);
});

it('should throw if parent is not SubmissionContainerElement', async () => {
const submissionItem = submissionItemFactory.build();
const richTextElement = richTextElementFactory.build();
boardDoRepo.findParentOfId.mockResolvedValueOnce(richTextElement);

await expect(service.update(submissionItem, true)).rejects.toThrow(UnprocessableEntityException);
});

it('should call bord repo to save submission item', async () => {
const { submissionItem, submissionContainer } = setup();

Expand Down
14 changes: 9 additions & 5 deletions apps/server/src/modules/board/service/submission-item.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ObjectId } from 'bson';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common';

import { EntityId, SubmissionContainerElement, SubmissionItem } from '@shared/domain';
import { EntityId, isSubmissionContainerElement, SubmissionContainerElement, SubmissionItem } from '@shared/domain';
import { ValidationError } from '@shared/common';

import { BoardDoRepo } from '../repo';
Expand Down Expand Up @@ -42,13 +42,17 @@ export class SubmissionItemService {
}

async update(submissionItem: SubmissionItem, completed: boolean): Promise<void> {
const parent = (await this.boardDoRepo.findParentOfId(submissionItem.id)) as SubmissionContainerElement;
const submissionContainterElement = await this.boardDoRepo.findParentOfId(submissionItem.id);
if (!isSubmissionContainerElement(submissionContainterElement)) {
throw new UnprocessableEntityException();
}

const now = new Date();
if (parent.dueDate && parent.dueDate < now) {
if (submissionContainterElement.dueDate && submissionContainterElement.dueDate < now) {
throw new ValidationError('not allowed to save anymore');
}
submissionItem.completed = completed;

await this.boardDoRepo.save(submissionItem, parent);
await this.boardDoRepo.save(submissionItem, submissionContainterElement);
}
}
27 changes: 18 additions & 9 deletions apps/server/src/modules/board/uc/element.uc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { AnyBoardDo, EntityId, SubmissionContainerElement, SubmissionItem, UserRoleEnum } from '@shared/domain';
import {
AnyBoardDo,
EntityId,
isSubmissionContainerElement,
isSubmissionItem,
SubmissionItem,
UserRoleEnum,
} from '@shared/domain';
import { Logger } from '@src/core/logger';
import { AuthorizationService } from '@src/modules/authorization';
import { Action } from '@src/modules/authorization/types/action.enum';
Expand Down Expand Up @@ -33,33 +40,35 @@ export class ElementUc {
contentElementId: EntityId,
completed: boolean
): Promise<SubmissionItem> {
const submissionContainer = (await this.elementService.findById(contentElementId)) as SubmissionContainerElement;
const submissionContainerElement = await this.elementService.findById(contentElementId);

if (!(submissionContainer instanceof SubmissionContainerElement))
if (!isSubmissionContainerElement(submissionContainerElement)) {
throw new HttpException(
'Cannot create submission-item for non submission-container-element',
HttpStatus.UNPROCESSABLE_ENTITY
);
}

if (!submissionContainer.children.every((child) => child instanceof SubmissionItem))
if (!submissionContainerElement.children.every((child) => isSubmissionItem(child))) {
throw new HttpException(
'Children of submission-container-element must be of type submission-item',
HttpStatus.UNPROCESSABLE_ENTITY
);
}

const userSubmissionExists = submissionContainer.children.find(
(item) => (item as SubmissionItem).userId === userId
);
const userSubmissionExists = submissionContainerElement.children
.filter(isSubmissionItem)
.find((item) => item.userId === userId);
if (userSubmissionExists) {
throw new HttpException(
'User is not allowed to have multiple submission-items per submission-container-element',
HttpStatus.NOT_ACCEPTABLE
);
}

await this.checkPermission(userId, submissionContainer, Action.read, UserRoleEnum.STUDENT);
await this.checkPermission(userId, submissionContainerElement, Action.read, UserRoleEnum.STUDENT);

const submissionItem = await this.submissionItemService.create(userId, submissionContainer, { completed });
const submissionItem = await this.submissionItemService.create(userId, submissionContainerElement, { completed });

return submissionItem;
}
Expand Down
24 changes: 1 addition & 23 deletions apps/server/src/modules/board/uc/submission-item.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,29 +194,7 @@ describe(SubmissionItemUc.name, () => {
it('should throw HttpException', async () => {
const { teacher, fileEl } = setup();

await expect(uc.findSubmissionItems(teacher.id, fileEl.id)).rejects.toThrow(
'Id does not belong to a submission container'
);
});
});
describe('when called with invalid submission container children', () => {
const setup = () => {
const teacher = userFactory.buildWithId();
const fileEl = fileElementFactory.build();
const submissionContainer = submissionContainerElementFactory.build({
children: [fileEl],
});
elementService.findById.mockResolvedValue(submissionContainer);

return { teacher, submissionContainer };
};

it('should throw HttpException', async () => {
const { teacher, submissionContainer } = setup();

await expect(uc.findSubmissionItems(teacher.id, submissionContainer.id)).rejects.toThrow(
'Children of submission-container-element must be of type submission-item'
);
await expect(uc.findSubmissionItems(teacher.id, fileEl.id)).rejects.toThrow('Id is not submission container');
});
});
});
Expand Down
35 changes: 11 additions & 24 deletions apps/server/src/modules/board/uc/submission-item.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { ForbiddenException, forwardRef, HttpException, HttpStatus, Inject, Inje
import {
AnyBoardDo,
EntityId,
SubmissionContainerElement,
isSubmissionContainerElement,
isSubmissionItem,
SubmissionItem,
UserBoardRoles,
UserRoleEnum,
Expand All @@ -29,22 +30,20 @@ export class SubmissionItemUc {
userId: EntityId,
submissionContainerId: EntityId
): Promise<{ submissionItems: SubmissionItem[]; users: UserBoardRoles[] }> {
const submissionContainer = await this.getSubmissionContainer(submissionContainerId);
await this.checkPermission(userId, submissionContainer, Action.read);
const submissionContainerElement = await this.elementService.findById(submissionContainerId);

let submissionItems = submissionContainer.children as SubmissionItem[];

if (!submissionItems.every((child) => child instanceof SubmissionItem)) {
throw new HttpException(
'Children of submission-container-element must be of type submission-item',
HttpStatus.UNPROCESSABLE_ENTITY
);
if (!isSubmissionContainerElement(submissionContainerElement)) {
throw new HttpException('Id is not submission container', HttpStatus.UNPROCESSABLE_ENTITY);
}

const boardAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(submissionContainer);
await this.checkPermission(userId, submissionContainerElement, Action.read);

let submissionItems = submissionContainerElement.children.filter(isSubmissionItem);

const boardAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(submissionContainerElement);
let users = boardAuthorizable.users.filter((user) => user.userRoleEnum === UserRoleEnum.STUDENT);

const isAuthorizedStudent = await this.isAuthorizedStudent(userId, submissionContainer);
const isAuthorizedStudent = await this.isAuthorizedStudent(userId, submissionContainerElement);
if (isAuthorizedStudent) {
submissionItems = submissionItems.filter((item) => item.userId === userId);
users = [];
Expand Down Expand Up @@ -86,18 +85,6 @@ export class SubmissionItemUc {
return false;
}

private async getSubmissionContainer(submissionContainerId: EntityId): Promise<SubmissionContainerElement> {
const submissionContainer = (await this.elementService.findById(
submissionContainerId
)) as SubmissionContainerElement;

if (!(submissionContainer instanceof SubmissionContainerElement)) {
throw new HttpException('Id does not belong to a submission container', HttpStatus.UNPROCESSABLE_ENTITY);
}

return submissionContainer;
}

private async checkPermission(
userId: EntityId,
boardDo: AnyBoardDo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ export interface SubmissionItemProps extends BoardCompositeProps {
completed: boolean;
userId: EntityId;
}

export function isSubmissionItem(reference: unknown): reference is SubmissionItem {
return reference instanceof SubmissionItem;
}

0 comments on commit 13f4d7a

Please sign in to comment.