Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/npm_and_yarn/golevelup/nestjs-rab…
Browse files Browse the repository at this point in the history
…bitmq-3.7.0
  • Loading branch information
CeEv committed Aug 31, 2023
2 parents a4f3403 + 3f64d6f commit 48fabff
Show file tree
Hide file tree
Showing 30 changed files with 1,193 additions and 196 deletions.
10 changes: 5 additions & 5 deletions apps/server/src/modules/board/board.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,25 @@ import {
@Module({
imports: [ConsoleWriterModule, FilesStorageClientModule, LoggerModule],
providers: [
BoardDoAuthorizableService,
BoardDoRepo,
BoardDoService,
BoardNodeRepo,
CardService,
ColumnBoardService,
ColumnService,
ContentElementService,
SubmissionItemService,
RecursiveDeleteVisitor,
ContentElementFactory,
BoardDoAuthorizableService,
CourseRepo, // TODO: import learnroom module instead. This is currently not possible due to dependency cycle with authorisation service
RecursiveDeleteVisitor,
SubmissionItemService,
],
exports: [
BoardDoAuthorizableService,
CardService,
ColumnBoardService,
ColumnService,
CardService,
ContentElementService,
BoardDoAuthorizableService,
SubmissionItemService,
],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardExternalReferenceType, SubmissionItemNode } from '@shared/domain';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
submissionContainerElementNodeFactory,
submissionItemNodeFactory,
} from '@shared/testing';
import { ServerTestModule } from '@src/modules/server';
import { SubmissionItemResponse } from '../dto';

const baseRouteName = '/board-submissions';
describe('submission item update (api)', () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ServerTestModule],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, baseRouteName);
});

afterAll(async () => {
await app.close();
});

describe('when user is a valid teacher', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const course = courseFactory.build({ teachers: [teacherUser] });
await em.persistAndFlush([teacherAccount, teacherUser, course]);

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 = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode });
const submissionItemNode = submissionItemNodeFactory.buildWithId({
userId: 'foo',
parent: submissionContainerNode,
completed: true,
});

await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, submissionItemNode };
};
it('should return status 403', async () => {
const { loggedInClient, submissionItemNode } = await setup();

const response = await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

expect(response.status).toEqual(403);
});
it('should not actually update submission item entity', async () => {
const { loggedInClient, submissionItemNode } = await setup();

await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id);
expect(result.completed).toEqual(submissionItemNode.completed);
});
});

describe('when user is a student trying to update his own submission item', () => {
const setup = async () => {
await cleanupCollections(em);

const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();
const course = courseFactory.build({ students: [studentUser] });
await em.persistAndFlush([studentAccount, studentUser, course]);

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 = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode });

const submissionItemNode = submissionItemNodeFactory.buildWithId({
userId: studentUser.id,
parent: submissionContainerNode,
completed: true,
});

await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(studentAccount);

return { loggedInClient, studentUser, submissionItemNode };
};
it('should return status 204', async () => {
const { loggedInClient, submissionItemNode } = await setup();

const response = await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

expect(response.status).toEqual(204);
});

it('should actually update the submission item', async () => {
const { loggedInClient, submissionItemNode } = await setup();
const response = await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

const submissionItemResponse = response.body as SubmissionItemResponse;

const result = await em.findOneOrFail(SubmissionItemNode, submissionItemResponse.id);
expect(result.id).toEqual(submissionItemNode.id);
expect(result.completed).toEqual(false);
});

it('should fail without params completed', async () => {
const { loggedInClient, submissionItemNode } = await setup();

const response = await loggedInClient.patch(`${submissionItemNode.id}`, {});
expect(response.status).toBe(400);
});
});

describe('when user is a student from same course, and tries to update a submission item he did not create himself', () => {
const setup = async () => {
await cleanupCollections(em);

const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();
const { studentAccount: studentAccount2, studentUser: studentUser2 } = UserAndAccountTestFactory.buildStudent();
const course = courseFactory.build({ students: [studentUser, studentUser2] });
await em.persistAndFlush([studentAccount, studentUser, studentAccount2, studentUser2, course]);

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 = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode });

const submissionItemNode = submissionItemNodeFactory.buildWithId({
userId: studentUser.id,
parent: submissionContainerNode,
completed: true,
});
await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(studentAccount2);

return { loggedInClient, submissionItemNode };
};

it('should return status 403', async () => {
const { loggedInClient, submissionItemNode } = await setup();

const response = await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

expect(response.status).toEqual(403);
});
it('should not actually update submission item entity', async () => {
const { loggedInClient, submissionItemNode } = await setup();

await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id);
expect(result.completed).toEqual(submissionItemNode.completed);
});
});

describe('when user is a student not in course', () => {
const setup = async () => {
await cleanupCollections(em);

const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();
const { studentAccount: studentAccount2, studentUser: studentUser2 } = UserAndAccountTestFactory.buildStudent();
const course = courseFactory.build({ students: [studentUser] });
await em.persistAndFlush([studentAccount, studentUser, studentAccount2, studentUser2, course]);

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 = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode });

const submissionItemNode = submissionItemNodeFactory.buildWithId({
userId: studentUser.id,
parent: submissionContainerNode,
completed: true,
});
await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(studentAccount2);

return { loggedInClient, submissionItemNode };
};

it('should return status 403', async () => {
const { loggedInClient, submissionItemNode } = await setup();

const response = await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

expect(response.status).toEqual(403);
});

it('should not actually update submission item entity', async () => {
const { loggedInClient, submissionItemNode } = await setup();

await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false });

const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id);
expect(result.completed).toEqual(submissionItemNode.completed);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Controller, ForbiddenException, Get, Param } from '@nestjs/common';
import { Body, Controller, ForbiddenException, Get, HttpCode, NotFoundException, Param, Patch } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiValidationError } from '@shared/common';
import { ICurrentUser } from '@src/modules/authentication';
import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator';
import { CardUc } from '../uc';
import { ElementUc } from '../uc/element.uc';
import { SubmissionItemUc } from '../uc/submission-item.uc';
import { BoardSubmissionIdParams, SubmissionItemResponse } from './dto';
import {
SubmissionContainerUrlParams,
SubmissionItemResponse,
SubmissionItemUrlParams,
UpdateSubmissionItemBodyParams,
} from './dto';
import { SubmissionItemResponseMapper } from './mapper';

@ApiTags('Board Submission')
Expand All @@ -26,10 +31,29 @@ export class BoardSubmissionController {
@Get(':submissionContainerId')
async getSubmissionItems(
@CurrentUser() currentUser: ICurrentUser,
@Param() urlParams: BoardSubmissionIdParams
@Param() urlParams: SubmissionContainerUrlParams
): Promise<SubmissionItemResponse[]> {
const items = await this.submissionItemUc.findSubmissionItems(currentUser.userId, urlParams.submissionContainerId);
const mapper = SubmissionItemResponseMapper.getInstance();
return items.map((item) => mapper.mapToResponse(item));
}

@ApiOperation({ summary: 'Update a single submission item.' })
@ApiResponse({ status: 204 })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@HttpCode(204)
@Patch(':submissionItemId')
async updateSubmissionItem(
@CurrentUser() currentUser: ICurrentUser,
@Param() urlParams: SubmissionItemUrlParams,
@Body() bodyParams: UpdateSubmissionItemBodyParams
) {
await this.submissionItemUc.updateSubmissionItem(
currentUser.userId,
urlParams.submissionItemId,
bodyParams.completed
);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './board-submission-id.params';
export * from './submission-container.url.params';
export * from './create-submission-item.body.params';
export * from './submission-item.response';
export * from './submission-item.url.params';
export * from './update-submission-item.body.params';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsMongoId } from 'class-validator';

export class BoardSubmissionIdParams {
export class SubmissionContainerUrlParams {
@IsMongoId()
@ApiProperty({
description: 'The id of the submission container.',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsMongoId } from 'class-validator';

export class SubmissionItemUrlParams {
@IsMongoId()
@ApiProperty({
description: 'The id of the submission item.',
required: true,
nullable: false,
})
submissionItemId!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean } from 'class-validator';

export class UpdateSubmissionItemBodyParams {
@IsBoolean()
@ApiProperty({
description: 'Boolean indicating whether the submission is completed.',
required: true,
})
completed!: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,26 +86,26 @@ export class ElementController {
await this.cardUc.deleteElement(currentUser.userId, urlParams.contentElementId);
}

@ApiOperation({ summary: 'Create a new submission item in a submission container element.' })
@ApiOperation({ summary: 'Create a new submission item having parent a submission container element.' })
@ApiExtraModels(SubmissionItemResponse)
@ApiResponse({ status: 201, type: SubmissionItemResponse })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@ApiBody({ required: true, type: CreateSubmissionItemBodyParams })
@Post(':contentElementId/submissions')
async createSubmission(
async createSubmissionItem(
@Param() urlParams: ContentElementUrlParams,
@Body() bodyParams: CreateSubmissionItemBodyParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<SubmissionItemResponse> {
const submission = await this.elementUc.createSubmissionItem(
const submissionItem = await this.elementUc.createSubmissionItem(
currentUser.userId,
urlParams.contentElementId,
bodyParams.completed
);
const mapper = SubmissionItemResponseMapper.getInstance();
const response = mapper.mapToResponse(submission);
const response = mapper.mapToResponse(submissionItem);

return response;
}
Expand Down
Loading

0 comments on commit 48fabff

Please sign in to comment.