Skip to content

Commit

Permalink
Merge branch 'main' into THR-18-dev-feature-h5p-editor
Browse files Browse the repository at this point in the history
  • Loading branch information
SteKrause authored Sep 29, 2023
2 parents 0c2dcc3 + f503620 commit ab0c79f
Show file tree
Hide file tree
Showing 127 changed files with 2,571 additions and 280 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardExternalReferenceType, ContentElementType, RichTextElementNode } from '@shared/domain';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
TestApiClient,
UserAndAccountTestFactory,
} from '@shared/testing';
import { ServerTestModule } from '@src/modules/server/server.module';
import { AnyContentElementResponse } from '../dto';
import { AnyContentElementResponse, SubmissionContainerElementResponse } from '../dto';

const baseRouteName = '/cards';

Expand Down Expand Up @@ -83,14 +83,23 @@ describe(`content element create (api)`, () => {
expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.FILE);
});

it('should return the created content element of type SUBMISSION_CONTAINER', async () => {
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.EXTERNAL_TOOL });

expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.EXTERNAL_TOOL);
});

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.SUBMISSION_CONTAINER,
});

expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.SUBMISSION_CONTAINER);
expect((response.body as SubmissionContainerElementResponse).content.dueDate).toBeNull();
});

it('should actually create the content element', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import {
FileElementNode,
InputFormat,
RichTextElementNode,
SubmissionContainerElementNode,
} from '@shared/domain';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
fileElementNodeFactory,
richTextElementNodeFactory,
submissionContainerElementNodeFactory,
TestApiClient,
UserAndAccountTestFactory,
} from '@shared/testing';
import { ServerTestModule } from '@src/modules/server/server.module';

Expand Down Expand Up @@ -59,29 +61,44 @@ describe(`content element update content (api)`, () => {

const column = columnNodeFactory.buildWithId({ parent: columnBoardNode });
const parentCard = cardNodeFactory.buildWithId({ parent: column });
const richTextelement = richTextElementNodeFactory.buildWithId({ parent: parentCard });
const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard });
const fileElement = fileElementNodeFactory.buildWithId({ parent: parentCard });
const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ parent: parentCard });

const tomorrow = new Date(Date.now() + 86400000);
const submissionContainerElementWithDueDate = submissionContainerElementNodeFactory.buildWithId({
parent: parentCard,
dueDate: tomorrow,
});

await em.persistAndFlush([
teacherAccount,
teacherUser,
parentCard,
column,
columnBoardNode,
richTextelement,
richTextElement,
fileElement,
submissionContainerElement,
submissionContainerElementWithDueDate,
]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, richTextelement, fileElement };
return {
loggedInClient,
richTextElement,
fileElement,
submissionContainerElement,
submissionContainerElementWithDueDate,
};
};

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

const response = await loggedInClient.patch(`${richTextelement.id}/content`, {
const response = await loggedInClient.patch(`${richTextElement.id}/content`, {
data: {
content: { text: 'hello world', inputFormat: InputFormat.RICH_TEXT_CK5 },
type: ContentElementType.RICH_TEXT,
Expand All @@ -92,30 +109,30 @@ describe(`content element update content (api)`, () => {
});

it('should actually change content of the element', async () => {
const { loggedInClient, richTextelement } = await setup();
const { loggedInClient, richTextElement } = await setup();

await loggedInClient.patch(`${richTextelement.id}/content`, {
await loggedInClient.patch(`${richTextElement.id}/content`, {
data: {
content: { text: 'hello world', inputFormat: InputFormat.RICH_TEXT_CK5 },
type: ContentElementType.RICH_TEXT,
},
});
const result = await em.findOneOrFail(RichTextElementNode, richTextelement.id);
const result = await em.findOneOrFail(RichTextElementNode, richTextElement.id);

expect(result.text).toEqual('hello world');
});

it('should sanitize rich text before changing content of the element', async () => {
const { loggedInClient, richTextelement } = await setup();
const { loggedInClient, richTextElement } = await setup();

const text = '<iframe>rich text 1</iframe> some more text';

const sanitizedText = sanitizeRichText(text, InputFormat.RICH_TEXT_CK5);

await loggedInClient.patch(`${richTextelement.id}/content`, {
await loggedInClient.patch(`${richTextElement.id}/content`, {
data: { content: { text, inputFormat: InputFormat.RICH_TEXT_CK5 }, type: ContentElementType.RICH_TEXT },
});
const result = await em.findOneOrFail(RichTextElementNode, richTextelement.id);
const result = await em.findOneOrFail(RichTextElementNode, richTextElement.id);

expect(result.text).toEqual(sanitizedText);
});
Expand Down Expand Up @@ -146,6 +163,76 @@ describe(`content element update content (api)`, () => {

expect(result.alternativeText).toEqual('rich text 1 some more text');
});

it('should return status 204 (nothing changed) without dueDate parameter for submission container element', async () => {
const { loggedInClient, submissionContainerElement } = await setup();

const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, {
data: {
content: {},
type: 'submissionContainer',
},
});

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

it('should not change dueDate value without dueDate parameter for submission container element', async () => {
const { loggedInClient, submissionContainerElement } = await setup();

await loggedInClient.patch(`${submissionContainerElement.id}/content`, {
data: {
content: {},
type: 'submissionContainer',
},
});
const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id);

expect(result.dueDate).toBeUndefined();
});

it('should set dueDate value when dueDate parameter is provided for submission container element', async () => {
const { loggedInClient, submissionContainerElement } = await setup();

const inThreeDays = new Date(Date.now() + 259200000);

await loggedInClient.patch(`${submissionContainerElement.id}/content`, {
data: {
content: { dueDate: inThreeDays },
type: 'submissionContainer',
},
});
const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id);

expect(result.dueDate).toEqual(inThreeDays);
});

it('should unset dueDate value when dueDate parameter is not provided for submission container element', async () => {
const { loggedInClient, submissionContainerElementWithDueDate } = await setup();

await loggedInClient.patch(`${submissionContainerElementWithDueDate.id}/content`, {
data: {
content: {},
type: 'submissionContainer',
},
});
const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElementWithDueDate.id);

expect(result.dueDate).toBeUndefined();
});

it('should return status 400 for wrong date format for submission container element', async () => {
const { loggedInClient, submissionContainerElement } = await setup();

const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, {
data: {
content: { dueDate: 'hello world' },
type: 'submissionContainer',
},
});

expect(response.statusCode).toEqual(400);
});
});

describe('with invalid user', () => {
Expand All @@ -163,24 +250,38 @@ describe(`content element update content (api)`, () => {

const column = columnNodeFactory.buildWithId({ parent: columnBoardNode });
const parentCard = cardNodeFactory.buildWithId({ parent: column });
const element = richTextElementNodeFactory.buildWithId({ parent: parentCard });
const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard });
const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ parent: parentCard });

await em.persistAndFlush([parentCard, column, columnBoardNode, element]);
await em.persistAndFlush([parentCard, column, columnBoardNode, richTextElement, submissionContainerElement]);
em.clear();

const loggedInClient = await testApiClient.login(invalidTeacherAccount);

return { loggedInClient, element };
return { loggedInClient, richTextElement, submissionContainerElement };
};

it('should return status 403', async () => {
const { loggedInClient, element } = await setup();
it('should return status 403 for rich text element', async () => {
const { loggedInClient, richTextElement } = await setup();

const response = await loggedInClient.patch(`${element.id}/content`, {
const response = await loggedInClient.patch(`${richTextElement.id}/content`, {
data: { content: { text: 'hello world', inputFormat: InputFormat.RICH_TEXT_CK5 }, type: 'richText' },
});

expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN);
});

it('should return status 403 for submission container element', async () => {
const { loggedInClient, submissionContainerElement } = await setup();

const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, {
data: {
content: {},
type: 'submissionContainer',
},
});

expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
userFactory,
} from '@shared/testing';
import { ServerTestModule } from '@src/modules/server';
import { SubmissionsResponse } from '../dto';
import { SubmissionsResponse } from '../dto/submission-item/submissions.response';

const baseRouteName = '/board-submissions';
describe('submission item lookup (api)', () => {
Expand Down
11 changes: 9 additions & 2 deletions apps/server/src/modules/board/controller/card.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import {
CardListResponse,
CardUrlParams,
CreateContentElementBodyParams,
ExternalToolElementResponse,
FileElementResponse,
MoveCardBodyParams,
RenameBodyParams,
RichTextElementResponse,
SubmissionContainerElementResponse,
} from './dto';
import { SetHeightBodyParams } from './dto/board/set-height.body.params';
import { RichTextElementResponse } from './dto/element/rich-text-element.response';
import { CardResponseMapper, ContentElementResponseFactory } from './mapper';

@ApiTags('Board Card')
Expand Down Expand Up @@ -114,14 +115,20 @@ export class CardController {
}

@ApiOperation({ summary: 'Create a new element on a card.' })
@ApiExtraModels(RichTextElementResponse, FileElementResponse, SubmissionContainerElementResponse)
@ApiExtraModels(
RichTextElementResponse,
FileElementResponse,
SubmissionContainerElementResponse,
ExternalToolElementResponse
)
@ApiResponse({
status: 201,
schema: {
oneOf: [
{ $ref: getSchemaPath(RichTextElementResponse) },
{ $ref: getSchemaPath(FileElementResponse) },
{ $ref: getSchemaPath(SubmissionContainerElementResponse) },
{ $ref: getSchemaPath(ExternalToolElementResponse) },
],
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger';
import { DecodeHtmlEntities } from '@shared/controller';
import { AnyContentElementResponse } from '../element';
import { AnyContentElementResponse, FileElementResponse, SubmissionContainerElementResponse } from '../element';
import { RichTextElementResponse } from '../element/rich-text-element.response';
import { TimestampsResponse } from '../timestamps.response';
import { VisibilitySettingsResponse } from './visibility-settings.response';
Expand Down Expand Up @@ -31,7 +31,11 @@ export class CardResponse {
@ApiProperty({
type: 'array',
items: {
oneOf: [{ $ref: getSchemaPath(RichTextElementResponse) }],
oneOf: [
{ $ref: getSchemaPath(RichTextElementResponse) },
{ $ref: getSchemaPath(FileElementResponse) },
{ $ref: getSchemaPath(SubmissionContainerElementResponse) },
],
},
})
elements: AnyContentElementResponse[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ExternalToolElementResponse } from './external-tool-element.response';
import { FileElementResponse } from './file-element.response';
import { RichTextElementResponse } from './rich-text-element.response';
import { SubmissionContainerElementResponse } from './submission-container-element.response';

export type AnyContentElementResponse =
| FileElementResponse
| RichTextElementResponse
| SubmissionContainerElementResponse;
| SubmissionContainerElementResponse
| ExternalToolElementResponse;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ContentElementType } from '@shared/domain';
import { TimestampsResponse } from '../timestamps.response';

export class ExternalToolElementContent {
constructor(props: ExternalToolElementContent) {
this.contextExternalToolId = props.contextExternalToolId;
}

@ApiPropertyOptional()
contextExternalToolId?: string;
}

export class ExternalToolElementResponse {
constructor(props: ExternalToolElementResponse) {
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.EXTERNAL_TOOL;

@ApiProperty()
content: ExternalToolElementContent;

@ApiProperty()
timestamps: TimestampsResponse;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './update-element-content.body.params';
export * from './file-element.response';
export * from './rich-text-element.response';
export * from './submission-container-element.response';
export * from './external-tool-element.response';
Loading

0 comments on commit ab0c79f

Please sign in to comment.